Handling Input and Output - Beginning Java Programming: The Object-Oriented Approach (Programmer to Programmer) (2015)

Beginning Java Programming: The Object-Oriented Approach (Programmer to Programmer) (2015)

8. Handling Input and Output

WHAT YOU WILL LEARN IN THIS CHAPTER:

· How input and output differ

· How to handle interaction with users in your programs

· How to store and load information in files

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

The wrox.com code downloads for this chapter are found at www.wrox.com/go/beginningjavaprogramming on the Download Code tab. The code is in the Chapter 8 download and individually named according to the names throughout the chapter.

So far, all the programs you’ve been writing throughout the course of this book have operated more or less on their own, without much interaction with the user at runtime, meaning that the interaction with the program happened while you were coding it, i.e. specifying all the tasks the program should perform. Once it ran, however, the program just went its course.

Interaction in programming is described as “input/output” (I/O). Of course, this communication flows in two directions, one in the form of “output,” which is information the program provides to outside parties, and the other in the form of “input,” which is information users provide to the program or information the program reads in from the outside world. It is easy to imagine a multitude of cases where such functionality could be useful. Imagine a program asking the user’s name, for instance, or a program asking if it should terminate or ignore an error when something unexpected happens. These aspects are all covered in this chapter.

Interaction can happen not only between a program and a human end user, but can also involve other sources of information. Consider, for example, the fact that so far, whenever you closed and restarted a program, all its previous data values were lost. When writing a budget tracking application, you cannot expect your users to leave the program running indefinitely (what if the power goes out?) or expect them to re-enter all the information once they reopen the program. As such, you will also deal with ways of handling I/O between your program and data sources. This chapter covers the most basic of data sources, namely that of a file.

GENERAL INPUT AND OUTPUT

When we talk about input and output in computing, we describe all forms of communication between a program on the computer and the “outside world,” which includes human end users, other programs on the same machine, or other programs running on other computers.

Input includes all the data and signals received by the running program. For instance, input can be sent to a program using input devices such as a keyboard or mouse, or can come from other computers, such as when you use your web browser to load in a specific web page. Output on the other hand includes all the signals and data sent from a program. Monitors and printers are prime examples of output devices, but again, output can involve pure data, such as when your web browser sends a request to a web server to receive a web page. The latter immediately illustrates that the same program (a web browser) can involve a series of input and output operations. The same holds for hardware devices, such as a network card or a modem.

A particular form of I/O we will be taking a closer look at in this chapter is file I/O, meaning input and output operations that read and write data to files stored on your computer. We can skip the details until we are ready to start dealing with files in Java, but two aspects are worth mentioning right now—file modes and the difference between text and binary files as they apply to programming languages other than Java.

Let’s start with file modes. In many programming languages, once you specify a particular file, the language requires you to specify a particular mode (which, in many cases, is passed on to the operating system). Two general modes are fairly obvious: opening a file for reading and opening a file for writing. However, other modes can be specified as well. The following list provides an enumeration of such file modes, together with their common abbreviations used in most programming languages:

· r: Open a file for reading only (pointer at beginning of file).

· r+: Open a file for reading and writing (pointer at beginning of file).

· w: Open a file for writing only (pointer at beginning of file); if the file does not exist, try to create it; existing files will be truncated (made empty).

· w+: Open a file for reading and writing (pointer at beginning of file); if the file does not exist, try to create it; existing file will be truncated (made empty).

· a: Open a file for writing only (pointer at end of file, i.e. append new data at the end of the file).

· a+: Open a file for reading and writing (pointer at end of file, i.e. append new data at the end of the file).

· x: Create a file and open for writing only; fail if file already exists.

· x+: Create a file and open for reading and writing; fail if file already exists.

· c and c+: Open a file for writing only or for reading and writing (pointer at beginning of file); if file does not exist, try to create it; existing files will not be made empty, as opposed to w/w+, which means that writing data will overwrite existing data.

Don’t panic if this list seems a bit daunting or confusing, as the most important thing for now is to remember that files can be opened for reading and writing. For writing, it is important to keep in mind that you can either empty an existing file and rewrite over it or keep the existing file in tact and append your data to the end. The reason why you don’t need to remember the full list of file modes is because the Java file API makes things simple, as you’ll see later.

The second general concept we highlight here is the difference between text and binary files. All the files on your computer can be categorized into these two formats. The difference lies in how the file encodes the data it contains. At the most basic level, both text and binary files store data as a series of bits (a series of zeroes and ones). However, in a text file, the bits represent characters (numbers, spaces, letters, etc.), while in a binary file, the bits represent custom data, stored and interpreted according to a particular format.

When you create a .txt file using Windows Notepad, for instance, you are creating a text file. Figure 8.1 illustrates a simple text file containing two lines of text opened in Notepad. On the right side, you can see this file in its hexadecimal representation—a compact representation of the bits stored in the file. As you can see, each byte (two hexadecimal characters) maps to exactly one character—54 maps to T and 73 maps to s.

images

Figure 8.1

CHARACTER ENCODING


You might be wondering how exactly your computer knows which byte (a series of eight zeroes and ones) should be mapped to which character. The answer lies in the concept of standardization. As with every standardization process, many standards exist, all of which are compatible with one another to varying degrees (ranging from fully backward-compatible to completely incompatible). In western regions, this standardization process started with standards such as EBCDIC (Extended Binary Coded Decimal Interchange Code), a character encoding scheme used by many IBM mainframes decades ago, and ASCII (American Standard Code for Information Interchange), which quickly became the standard in the early days of computing.

However, as time progressed, many vendors were confronted with the limitations of this American-centric standard, as no mapping was provided to represent accented characters (such as è, ï, and so on) used in non-English languages. As such, the International Organization for Standardization (ISO) and the International Electrotechnical Commission (IEC) proposed the ISO/IEC 8859 standard, a collection of character encodings to support European, Cyrillic, Greek, Turkish, and other languages. As ASCII used only seven out of eight bits provided in a byte (the remaining bit was sometimes used to calculate a checksum or used by vendors to map custom characters), the implementation of this standard was simple. By using the eighth bit, the range of possible characters that could be represented doubled and could thus include the accented characters. Moreover, this also ensured that all the ASCII characters could still retain their original position, which enabled backward-compatibility with existing text files.

Still, a downside of this approach was that users had to have the correct code page installed and selected on their system in order to read text files correctly. Whereas I might create a text file following the ISO/IEC 8859-1 convention (western Latin alphabet), the result will look wrong if you read the file using ISO/IEC 8859-7 (Greek). In addition, many languages were still not covered by the extending standards. Consider, for example, Asian regions, where a completely different standardization process had been followed thus far (the Chinese National Standard 11643). As such, in recent years, the ISO and IEC set out to create another standard called the Universal Character Set. This standard aims to represent all characters from the many languages of the world. The implementation comes in the form of the “Unicode” standard, the latest version of which contains a repertoire of more than 100,000 characters covering 100 scripts and various symbols. This is the standard now in use by all modern operating systems and throughout the Web.

Various encodings have been defined in order to map this wealth of characters to raw bits and bytes in a file. UTF-8 is the most common encoding format and it uses one byte for any ASCII character, all of which have the same code values in both UTF-8 and ASCII encoding (which is great news in terms of compatibility). For other characters, up to four bytes can be used. (As such, UTF-8 is called a “variable width” encoding.) Next, UCS-2 uses a two-byte code unit for each character, and thus cannot encode every character in the Unicode standard. UTF-16 extends UCS-2, using two-byte code units for the characters that were representable in UCS-2 and four-byte code units to handle each of the additional characters.

This might all seem a bit overwhelming, but the good news is that in recent years, thanks to the Unicode Consortium, things have become much simpler. The key takeaways to keep in mind are:

· When saving files in the old ASCII standard, you ensure compatibility but can only store a basic range of characters (fine for English).

· Otherwise, try to work with UTF-8. This ensures that every ASCII character is still represented by one byte, and all other characters will take up a few more bytes.

Binary files are much less structured, in the sense that they contain a sequence of bits that are structured and organized completely according to the whims of the program or programmer who created the file. This does not mean, however, that there cannot be some form of standardization behind such files. Consider, for example, image formats such as JPG or PNG, which can be opened by many image-viewing programs. Consider Figure 8.2, which shows a JPG file of a fractal opened by an image viewer capable of interpreting this format on the left side and the raw bytes on the right side.

images

Figure 8.2

As you can see in Figure 8.2, the contents of this binary file cannot be represented as a series of characters in a meaningful way, although fragments and pieces of text can exist here and there, representing metadata (where the picture was taken, for instance) or strings. In many cases, many binary files will also start with a specific sequence of bytes (called a “magic number”), denoting what type of file it is. JPG files, for instance, begin with FF D8.

SPECIAL CHARACTERS


There's more to text files than character encodings alone. When dealing with text, not every character necessarily needs to represent a letter or a number. Consider for instance a special “character” representing a space or a tab, or a character representing a line break (the end of a line). Especially regarding the latter, some differences exist between operating systems. On Windows, a line break is represented by two characters (0D and 0A in hexadecimal). On most Unix-derived operating systems (such as Linux), a line break is represented by a single character (0A). The latter also holds for Mac systems, except for older Macs, where 0D is used instead. Some older operating systems also use fixed line lengths or other characters, but suffice it to say that this is something not all systems agree on. Luckily, Java helps correctly detect the end of a line in a text file, as you will see later.

INPUT AND OUTPUT IN JAVA

Dealing with input and output in Java—especially when working with files—is a topic that beginning Java programmers typically view in a begrudging manner, especially when coming in from other programming languages and looking up examples that are not yet up to par with the latest Java versions. Let’s illustrate the reasons behind this with a typical example. Say you have a simple text file of a grocery list and want to go through it line by line in your program. Traditionally, examples, tutorials, and textbooks would have suggested a solution that looks like the following:

import java.io.BufferedReader;

import java.io.FileReader;

public class ShowGroceries {

public static void main(String[] args) {

BufferedReader br = null;

FileReader fr = null;

try {

fr = new FileReader("groceries.txt");

br = new BufferedReader(fr);

String line;

while ((line = br.readLine()) != null) {

System.out.println("Don't forget to pickup: " + item);

}

} catch (Exception x) {

x.printStackTrace();

} finally {

if (fr != null) {

try {br.close();} catch (Exception e) { e.printStackTrace(); }

try {fr.close();} catch (Exception e) { e.printStackTrace(); }

}

}

}

}

What is up with this large try-catch block? What is a FileReader? What is a BufferedReader? At first sight, dealing with file input/output in Java seems cumbersome and confusing.

A similar reasoning holds for basic user interaction. You’ve already seen in many examples how to show output to the user console using System.out:

public class JavaInput {

public static void main(String[] args) {

System.out.println("What is your name, user?");

}

}

Reading input given by the user, however, requires a more convuluted setup:

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

public class JavaInput {

public static void main(String[] args) {

System.out.println("What is your name, user?");

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String name;

try {

name = br.readLine();

} catch (IOException e) {

name = "?";

e.printStackTrace();

} finally {

try { br.close(); } catch (IOException e) { e.printStackTrace(); }

}

System.out.println("Welcome, " + name);

}

}

You might be bothered by the presence of the try-catch blocks and the occurrence of the InputStreamReader, as well as the BufferedReader, which makes another appearance.

Don’t despair, however, as the design choices behind this, together with arcane names such as InputStreamReader, will all become clear in the following sections. The reason why Java’s input/output model looks somewhat daunting at first is because it is based on so-called “I/O streams.” A stream is simply an abstraction of a particular input source (any kind) or an output destination (also of any kind), meaning that streams can represent files, a program console, other programs, memory locations, or even hardware devices. Depending on the input source or output destination accessed, streams can support a variety of data formats, such as raw bits and bytes, but also characters, primitive data types, and even complete objects. Some streams will just pass on data, whereas others will perform some transformations. The key point, however, is that at their core, streams all represent a sequence of data. While this stream-based model can be somewhat verbose in the beginning, a great advantage is that they offer a unified model to deal with input/output, meaning that if you know how to send output to a stream with a file as its destination, you also know how to output data to the user console or to a device stream.

Even better news for beginning Java programmers is that, since Java 7, new features were added that greatly simplify the examples above. Sadly, these additions have also made the I/O landscape in Java slightly more chaotic. In Java SE 1.4, a new “Non-Blocking IO” API—oftentimes called NIO and referred to as “new I/O”—was added to the language to complement the existing I/O facilities. In Java 7, extensions were added in the form of NIO2 to offer a new, more sensible file system API. However, since backward-compatibility is a strong design goal behind Java, existing file I/O methods remained supported, so that many code samples, books, tutorials, and real-life code still apply “legacy I/O” API features. Is this a problem? Not really, except for the fact that you, the beginning Java programmer, will have to deal with both APIs, hence the illustrating examples above. The general recommendation for new projects is to use as much NIO2-based code as possible, but reverting back to the legacy API where necessary is fine. In fact, the Java language designers foresaw this issue and created a set of intercompatibility methods to quickly switch between the two, as you will see later.

NOTE As an example of how the NIO2 API improves file I/O in Java, consider the “read a grocery list” example provided earlier. Using NIO2, this code fragment can be rewritten as follows:

import java.io.IOException;

import java.nio.charset.Charset;

import java.nio.file.Files;

import java.nio.file.Paths;

import java.util.ArrayList;

import java.util.List;

public class ShowGroceries {

public static void main(String[] args) {

List<String> groceries = new ArrayList<>();

try {

groceries = Files.readAllLines(

Paths.get("groceries.txt"),

Charset.defaultCharset());

} catch (IOException | SecurityException e) {

e.printStackTrace();

}

for (String item : groceries) {

System.out.println("Don't forget to pickup: " + item);

}

}

}

Notice the readAllLines method, which removes the burden of having to use the FileReader and BufferedReader classes.

You are now ready to delve into I/O with Java for real and explore the concept of streams in more detail.

STREAMS

We’ve mentioned before that the key abstraction behind I/O in Java is the concept of a stream. Streams represent a sequence of data as it is read from a source (an input stream) or written to a destination (an output stream). This immediately clarifies why you will not deal with file modes in Java (opening a file for reading or writing), as the type of stream (input or output) will determine whether you want to use a file as a data source or destination.

We’ve also stated that streams can support different kinds of data, and depending on the data type they carry, different methods are exposed. For streams carrying textual data, for instance (think back on text files), it makes sense to read in a single text line. For streams carrying byte data (raw bits and bytes), no concept of a “text line” can be defined, so this method does not make sense in this context. Apart from reading and writing data, streams can also modify or transform data as it passes through. A common example is an output stream that zips (compresses) the data while writing it to a file, in order to reduce the file size.

The following sections discuss the various types of streams in more detail.

Byte Streams

Byte streams represent sequences of data that’s represented at its most basic, raw format, meaning bytes (eight bits). They form the lowest level of I/O in Java so that all other streams are built on them.

All byte streams subclass InputStream and OutputStream (inheritance, subclasses, superclasses, and interfaces were explained in the previous chapter) and always at least provide the following methods. For InputStream:

· int read(): Reads the next byte of data from the input stream. Returns -1 if the end of the stream is reached.

· int read(byte[] b): Reads some number of bytes from the input stream and stores them into the array b. Returns the total number of bytes read into the buffer or -1 if there is no more data because the end of the stream has been reached.

· int read(byte[] b, int off, int len): Reads up to len bytes of data from the input stream at offset off into an array b. Returns the total number of bytes read into the buffer or -1 if there is no more data because the end of the stream has been reached.

· long skip(long n): Skips over and discards n bytes of data from this input stream. Returns the actual number of bytes skipped.

· void close(): Closes this input stream and releases any system resources associated with it.

· int available(): Returns an estimate of the number of bytes that can be read (or skipped over) from this input stream without blocking.

· boolean markSupported(): Tests if this input stream supports the mark and reset methods.

· void mark(int readlimit): Marks the current position in this input stream.

· void reset(): Repositions this stream to the position at the time the mark method was last called on this input stream.

INTEGERS AS BYTES


You might be surprised by the fact that the read methods in the previous list return ints and not bytes, which is also a primitive data type and would be a logical sounding choice for a stream-reading and -writing byte. The reason for this is twofold. First, the byte type in Java is signed, meaning that after conversion to a number, it represents the range of -128 to 127, which makes calculation more cumbersome when an unsigned number ranging between 0 and 255 is expected. Furthermore, note that theread methods return -1 when the end of a stream is reached, which would then not be able to fall inside the range of an 8-bit range (0 to 255).

Very astute readers will note that the short primitive data type would also be fine to represent a byte, as it ranges from -32,768 to 32,767 and thus has plenty of space to represent both -1 and 0 to 255. The reason for this is simply because working withints is faster than shorts in the JVM (as this 32-bit type maps more closely to underlying modern hardware), and that int has become sort of a standard means to hold a byte of data. Secondly, you might also wonder why read returns -1 instead of throwing an exception when the end of a stream is reached. Again a good point, and this is due to historic reasons and standardized practices (coming mainly from the world of the C programming language).

For OutputStream, we get:

· void write(int b): Writes the specified byte (represented using an int variable) to this output stream.

· void write(byte[] b): Writes the specified byte array b to this output stream.

· void write(byte[] b, int off, int len): Writes len bytes from the b starting at offset off to this output stream.

· void close(): Closes this output stream and releases any system resources associated with this stream.

· void flush(): “Flushes” this output stream, i.e. forces any buffered output bytes to be written out.

It’s worth underlining the fact that the close method appears in both the InputStream and OutputStream classes. It is extremely important to always close streams when you no longer need them. In fact, this is so important that, even when an error occurs, you should still attempt to close streams. Keeping streams open can lead to resource leaks, i.e. files remain opened by the JVM and take up memory in your program. When the data source or destination is something other than a file, the close method of the byte stream class might also be responsible for ensuring that everything is tidied up correcly.

To get you started with byte streams, the following Try It Out shows you how to copy a file using FileInputStream and FileOutputStream, two basic byte stream classes aimed at working with files.

TRY IT OUT Copying Files with Byte Streams

In this Try It Out, you will copy a grocery list using byte streams.

1. Create a new project in Eclipse if you haven’t done so already.

2. In the Eclipse Package Explorer, create a new file called groceries.txt within the project root (not in the src folder). See Figure 8.3.images

Figure 8.3

3. Enter a grocery list in this TXT file and save it (one item per line). This example uses the following shopping list:

4. apples

5. bananas

6. water

7. orange juice

8. milk

bread

9. Create a FileCopier class with the following contents:

10. import java.io.FileInputStream;

11. import java.io.FileOutputStream;

12. import java.io.IOException;

13.

14. public class FileCopier {

15. public static void main(String[] args) {

16. FileInputStream in = null;

17. FileOutputStream out = null;

18. try {

19. in = new FileInputStream("groceries.txt");

20. out = new FileOutputStream("groceries (copy).txt");

21. int c;

22. while ((c = in.read()) != -1) {

23. out.write(c);

24. System.out.print((char) c);

25. }

26. } catch (IOException e) {

27. e.printStackTrace();

28. } finally {

29. if (in != null) try { in.close(); } catch (IOException e)

30. { e.printStackTrace(); }

31. if (out != null) try { out.close(); } catch (IOException e)

32. { e.printStackTrace(); }

33. }

34. }

}

35.Execute the program. If you refresh your project (right-click and select Refresh or press F5), you will see that a file called groceries (copy).txt has appeared. In addition, the copied bytes are shown on Eclipse’s console.

How It Works

Here’s how it works:

1. We’re using both a FileInputStream and FileOutputStream class here to open the original file as a data source and a new file as a destination. If the file does not exist, its creation will be attempted. If the file does exist, its contents are overridden, unless you pass true as the second argument of the FileOutputStream constructor.

2. Next, we keep reading bytes from the original file until we reach the end, in which case the read method will return -1. Inside the while loop, we use the write counterpart method of the FileOutputStream to write the read byte to the new file.

3. We also show the contents as they are copied on the console, so you can follow along as the file is being copied (the System.out.print line can be safely removed). To do so, we cannot just print the int c variable, as doing so would result in a series of numbers as output (you can try this yourself). Therefore, we first cast the integer to a character. Note that this is a somewhat crude approach that assumes that we are dealing with readable, text-based data (which is not necessarily the case when working with byte streams) and that the original data is stored using a standard ASCII-based encoding. For the sake of this example, these assumptions are not problematic, though.

4. An IOException is caught in case some error occurs (for instance, when the original file does not exist). A finally block closes the streams. Note that closing streams can also throw exceptions, but we just give up in that case and ignore those.

5. Note that streams provide an excellent opportunity to make use of a Java 7–specific feature called ARM (Automatic Resource Management), also known under the name try-with-resources. As you saw in Chapter 6, a try-with-resources block is a trystatement that declares one or more resources. These resources will be closed at the end of the code included in the try block. Any objects that implement the AutoCloseable interface (which includes streams) can be used in such a try-with-resources block. As such, the previous code can be rewritten as follows:

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

public class FileCopier {

public static void main(String[] args) {

try (

FileInputStream in = new FileInputStream("groceries.txt");

FileOutputStream out = new FileOutputStream("groceries (copy).txt");

) {

int c;

while ((c = in.read()) != -1) {

out.write(c);

System.out.print((char) c);

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

The PrintStream class is another interesting example that implements extra methods to allow for easy output formatting on top of a normal byte output stream:

· void print(String s): Writes the specified string to this print stream. Note that this method is overridden to also accept other primitive types (char, double, etc.).

· void println(String s): Writes the specified string to this print stream, but also terminates the line (i.e. starts a new line). Note that this method is overridden to also accept other primitive types (char, double, etc.).

· void format(String format, Object... args): This function takes a “format string” as its first argument and the variables you want to format as the following arguments. The format string contains a number of percentage (%) fields representing where, which, and how variables should be formatted. The most basic conversions are:

· %d: to format an integer value as a decimal value

· %f: to format a floating-point value as a decimal value

· %n: to output a line terminator

· %x: to format an integer value as a hexadecimal value (less useful)

· %s: to format any value as a string (effectively calling the toString method)

· %%: to output a percentage sign

Note that the System.out object we’ve been using to send output to the console is in fact an example of a PrintStream. It is opened at the start of your program and closed automatically at the end, so you can use it directly without further management. The following Try It Out shows some examples of PrintStream’s methods.

TRY IT OUT Formatting Output with PrintStream

This Try It Out shows some examples of formatting output with a PrintStream byte stream.

1. Create a new project in Eclipse if you haven’t done so already.

2. Create the following class and execute it to see the examples in action:

3. public class FormattingOutput {

4. public static void main(String[] args) {

5. /* System.out is a PrintStream, but a PrintStream class

6. is a specialization of an OutputStream. The write() method

7. is thus available: */

8. System.out.write(50); // 50 corresponds to '2'

9. System.out.write((int)'\n'); // newline

10.

11. // However, it is much easier to use print and println:

12. System.out.print("Text without newline");

13.

14. System.out.print("\r\nYou can enter a newline\r\n"

15. + "manually, as well as tabs using \t tab \t tab \t ...\r\n"

16. + "Backslashes themselves are entered with \\...\r\n");

17.

18. System.out.println("println is easier to show a "

19. + "string with a newline");

20.

21. // The format method can be used to format arguments in a string

22. int number = 10;

23. double othernumber = 1.134;

24. System.out.println("Using + is okay in most cases: " + number

25. + ", " + othernumber);

26. System.out.format("But format allows for more flexibility: %d, %3.2f %n",

27. number, othernumber);

28. System.out.format("Another %3$s: %2$+020.10f, %1$d%n",

29. number, othernumber, "example");

30. }

}

31. Executing this program shows the following output in the console. Try experimenting with the print, println, and format methods at your own leisure.

32. 2

33. Text without newline

34. You can enter a newline

35. manually, as well as tabs using tab tab ...

36. Backslashes themselves are entered with \...

37. println is easier to show a string with a newline

38. Using + is okay in most cases: 10, 1.134

39. But format allows for more flexibility: 10, 1.13

Another example: +00000001.1340000000, 10

How It Works

Here’s how it works:

1. We’re using the standard System.out PrintStream to illustrate the workings of this class.

2. Since a PrintStream extends an OutputStream, you can still call the write method on System.out as well. The code sample does so by first sending the byte 50 (represented using an int variable and corresponding to the character 2) and a newline character (by casting it to an int) to System.out.

3. The workings of the print and println methods should be familiar by now. Note, however, the use of \n, \r, \t, and \\ within the strings to insert special characters. These special backslash combinations are called “escape sequences.”

4. The format method on the other hand is new. As explained above, this function takes a “format string” as its first argument and the variables you want to format as the following arguments.

5. Using this method, numbers can be formatted with advanced formatting parameters. For example, %3.2f in our code shows “1.134” as “1.13”. The full set of formatting parameters are given by the example %2$+020.10f, with:

· 2$ denotes the argument index (2). Use this if the variables to be formatted do not follow the same order as provided in the format string or if you want to output the same variable more than once.

· +0 for flags. This is a series of characters including + to specify that a number should be formatted with a sign, 0 to specify that 0 is the padding character (if not provided, a space is used instead), - to specify that padding should be added on the right (the number will be left aligned), and , to specify that a locale-specific thousands separator should be used.

· 20 is the minimum width of the formatted value. The value will be padded if necessary to obtain this width.

· .10 is the precision (10). For decimals, this is the mathematical precision of the formatted value. For %s and other conversions, this is the maximum width of the formatted value, truncated if necessary.

· f is the actual conversion.

Returning to the file copy program we introduced before, recall that we’re copying the file in a raw, byte-by-byte manner. While this is fine, byte streams actually represent low-level input/output. Since the grocery list is a text file containing character data, it might be better to use a higher-level stream type geared more toward this purpose, namely character streams.

Character Streams

Character streams translate Unicode characters (used internally within Java) to and from the character locale specified. This stream is equal to a byte stream but adapts to the local character set and is thus ready for internationalization.

All character streams subclass Reader and Writer, e.g., FileReader and FileWriter. These classes are not subclasses of the byte stream InputStream and OutputStream classes, but offer a similar set of methods, e.g., read and write. The next Try It Out reworks the file copier example to use character streams instead of byte streams.

TRY IT OUT Copying Files with Character Streams

In this Try It Out, you will copy a grocery list using character streams.

1. We will modify the FileCopier class we created earlier. Refer to the earlier Try It Out if you have not done so already. Make sure the groceries.txt file is still present.

2. Edit the FileCopier class to look as follows:

3. import java.io.FileReader;

4. import java.io.FileWriter;

5. import java.io.IOException;

6.

7. public class FileCopier {

8. public static void main(String[] args) {

9. try (

10. Reader in = new FileReader("groceries.txt");

11. Writer out = new FileWriter("groceries (copy).txt");

12. ) {

13. int c;

14. while ((c = in.read()) != -1) {

15. out.write(c);

16. System.out.print((char) c);

17. }

18. } catch (IOException e) {

19. e.printStackTrace();

20. }

21. }

}

22.Execute the program. You’ll see that it behaves similarly as before.

How It Works

Here’s how it works:

1. We’re now using the FileReader and FileWriter classes here to open the original file as a data source and a new file as a destination. If the file does not exist, the program will attempt to create it. If the file does exist, its contents are overridden, unless you pass true as the second argument of the FileWriter constructor.

2. The rest of the program is performed very similarly to using byte streams. Note that we’re using a try-with-resources block to handle the closing of the Reader and the Writer automatically. If you use a traditional try-catch block, do not forget to add afinally clause when you close the Reader and Writer.

3. With our Reader and Writer acting so similarly to our InputStream and OutputStream, you might be wondering why it makes sense to use character streams in the first place. The first difference lies in the fact that Readers and Writers store characters in the last 16 bits of an int, and thus support a wider range of characters. Byte streams read and write bytes, that is, they store a byte in the last eight bits of an int. For this simple English grocery list, the difference is not noticeable, but once you start adding a wider range of characters, you’ll see that character streams are the right route to follow. Second, when dealing with text files, you might often be interested in working with bigger units than just a single byte (or character), for example to read and show lines. With Readers and Writers, you can do so, although you first need to add another stream type to the mix, as you’ll see in the following section on buffered streams.

Next, note the existance of InputStreamReader and OutputStreamReader to create byte-to-character bridges when no native character stream class exists to meet your data source/destination (you will see a situation later on where this becomes useful). The PrintWriterclass, finally, is the character stream counterpart of the PrintStream class.

Buffered Streams

Most streams in Java are unbuffered, meaning that each write and read request is handled directly by the operating system. This makes programs less efficient, as every write request to a file, for instance, will trigger disk access or some other time-consuming operation. Therefore, buffered streams can be used to wrap around other streams. They provide a dedicated space in memory (a buffer) to store data in an efficient manner, and will request time-expensive operations only if necessary (such as when the buffer is full and ready to be written to disk).

Four buffer classes exist which can be wrapped around a byte or character input/output stream: BufferedInputStream, BufferedOutputStream, BufferedReader, and BufferedWriter. The latter two classes are especially helpful when dealing with text files, as they allow you to work with data in a line-oriented manner. The following Try It Out once again shows the file copier example rewritten.

TRY IT OUT Copying and Showing Files Line by Line with Buffered Streams

In this Try It Out, you will copy a grocery list using buffered character streams.

1. You will modify the FileCopier class created earlier. Refer to the earlier Try It Out if you have not done so already. Make sure the groceries.txt file is still present.

2. Edit the FileCopier class to look as follows:

3. import java.io.BufferedReader;

4. import java.io.BufferedWriter;

5. import java.io.FileReader;

6. import java.io.FileWriter;

7. import java.io.IOException;

8.

9. public class FileCopier {

10. public static void main(String[] args) {

11. try (

12. Reader in = new BufferedReader(

13. new FileReader("groceries.txt"));

14. Writer out = new BufferedWriter(

15. new FileWriter("groceries (copy).txt"));

16. ) {

17. String line;

18. while ((line = in.readLine()) != null) {

19. out.write(line + System.lineSeparator());

20. System.out.println(line);

21. }

22. } catch (IOException e) {

23. e.printStackTrace();

24. }

25. }

}

26.Execute the program. You’ll see that it behaves similarly as before.

How It Works

Here’s how it works:

1. We’re now using BufferedReader and BufferedWriter classes here to open the original file as a data source and a new file as a destination. Note that these classes wrap around normal Reader and Writer classes (just as BufferedInputStream andBufferedOutputStream wrap around InputStream and OutputStream classes).

2. Note earlier that we mentioned that you can use InputStreamReader and OutputStreamReader to create byte-to-character bridges. It is therefore, in theory, possible to write the following:

3. BufferedReader in = new BufferedReader(

4. new InputStreamReader(

new FileInputStream("groceries.txt")));

But it goes without saying that this is not an efficient nor clear manner to achieve the desired effect. Therefore, only use InputStreamReader and OutputStreamReader when no other option is available.

5. The rest of the program works similarly, but now reads data line by line. Note that the newline terminator will be stripped from the read line, so we use System.lineSeparator() to add a platform-dependent line ending (\r or \n or \r\n) when writing the line to the output file. Note that the system line separator might be different than the one used in the original file, so this implementation of our file copier might not make exact copies in this case (if you do want to create an exact copy, you can use the read() method, but this will result in the code working more slowly, as now every character is read, returned, and copied one by one). Note also that the readLine method returns null when the end of a stream is reached.

NOTE When not using try-with-resources blocks and closing your streams manually, you might wonder which streams you need to close when wrapping streams (such as is the case with buffered streams) and in which order. The answer is simple: just close the topmost stream. The underlying ones will be closed and tidied up as well.

Data and Object Streams

Data streams support binary input and output of primitive data type values (boolean, char, byte, short, int, long, float, and double) as well as String values. They thus offer a simple generic means to load and store primitive data values. All data streams implement either the DataInput interface or the DataOutput interface, i.e. DataInputStream and DataOutputStream. Note that these streams subclass InputStream and OutputStream and are thus a specialization of byte streams.

Object streams are similar to data streams, but allow the serialization of all objects that implement the Serializable marker interface. The object streams implement either the ObjectInput or ObjectOutput interfaces (which themselves are subinterfaces of DataInput andDataOutput), i.e. ObjectInputStream and ObjectOutputStream. Note that these streams subclass InputStream and OutputStream and are thus a specialization of byte streams.

The following Try It Out shows you how to work with data and object streams.

TRY IT OUT Working with Data and Object Streams

In this Try It Out, you create a small class to illustrate the workings of an ObjectOutputStream. Note that an ObjectOutputStream can write primitive data types as well (as a DataOutputStream using the same method names), but can also serialize objects.

1. Create a class called ObjectOutputStreamTest with the following contents:

2. import java.io.FileOutputStream;

3. import java.io.IOException;

4. import java.io.ObjectOutputStream;

5. import java.util.ArrayList;

6. import java.util.List;

7.

8. public class ObjectOutputStreamTest {

9. public static void main(String[] args) {

10. int number1 = 5;

11. double number2 = 10.3;

12. String string = "a string";

13. List<String> list = new ArrayList<>();

14. list.add("a");

15. list.add("b");

16.

17. try (

18. ObjectOutputStream out = new ObjectOutputStream(

19. new FileOutputStream("saved.txt"));

20. ) {

21. out.writeInt(number1);

22. out.writeDouble(number2);

23. out.writeBytes(string);

24. out.writeObject(list);

25. } catch (IOException e) {

26. e.printStackTrace();

27. }

28. }

}

29.Execute the program and refresh your Eclipse project. A file called saved.txt should appear with contents similar to these:

@$℡℡℡℡℡ša string¬í sr FileCopier$1ÍÉJ;¹–

\--------------------------------------------------------------------------------

xr java.util.ArrayListxÒ&traed;Ça•---------------------------------

I ----------------------------------------------------------------------------------

sizexpw-------------------------------------------------------------------------------

t at bx¬í w @$℡℡℡℡℡ša stringsr FileCopier$1ÍÉJ;¹

\--------------------------------------------------------------------------------

xr java.util.ArrayListxÒÇa-------------------------------------- I

-------------------------------------------------------------------------------

sizexpw-------------------------------------------------------------------------------

t at bx

How It Works

Here’s how it works:

1. The ObjectOutputStream class is created as a wrapper around a normal FileOutputStream.

2. Next, ObjectOutputStream exposes a number of methods, i.e. writeInt, writeDouble, and so on, to write data to the stream.

3. Although you save the data to a file named saved.txt, the reality is that you are creating a binary file and not a text file, which explains why the text looks all garbled when you try to open it with a text editor. You are looking at raw binary data representing primitive values and objects, stored according to a format defined by the JVM.

4. Things to try: change ObjectOutputStream to DataOutputStream. Which variables can be written? Which cannot? Try expanding this class to use ObjectInputStream to read the saved data back in again. Take care to read the data in the correct order as saved in the file.

A common use case for using data and object streams is to implement input and output (communication) between different running programs, for example to send objects or data from one process to another. This concept is called inter-process communication (IPC). While discussing the details of IPC is too advanced for a beginner’s book on programming, it is worth knowing that this exists and can in fact be implemented in various ways:

· Using a file: One program writes information to a file, which is then read out by another program. This method is crude, but works and is relatively simple to implement. After reading through this chapter, you’ll know how to read and write from files, and the Data and Object streams mentioned above can be utilized here to read from and write to these files.

· Sending a signal: This works only if signaling capabilities are provided by the underlying operating system. Oftentimes it’s used to send commands instead of larger data.

· A network socket: A data stream sent over a network interface. Note that this allows the communicating programs to also reside on different machines. Chapter 10 provides a basic introduction regarding building web services using Java, which can also be used to enable IPC.

· A pipe: This is a data stream that allows two-way character-by-character communication, supported and provided by most operating systems.

· A message queue: Also provided by the operating system. Similar to a pipe, but messages are sent in packets rather than streamed character by character.

· Shared memory: Two programs are given access to the same part of memory.

A WORD ABOUT SERIALIZATION


We have stated that objects can be saved and read using ObjectOutputStream and ObjectInputStream as long as they implement the Serializable marker interface. We call this a “marker” interface, as the interface does not actually specify any methods that must be implemented by the class that is to be serialized. Any object can be serialized, as long as it implements this marker interface and all its fields can be serialized, meaning that the fields that should be primitive types are other objects that can be serialized (implementing the Serializable interface).

Note that it is possible to specifiy fields that should not be serialized using the transient keyword, for instance:

private transitent String secretCreditCardNumber;

The reason why you might want to make fields transient is because they are stored in an unencrypted format when they're serialized.

Finally, note that you can override two hidden methods: private void readObject (ObjectInputStream s) and private void writeObject(ObjectOutputStream s) in order to define a custom (de)serialization scheme. In most cases, the default serialization scheme works just fine, though.

Other Streams

Finally, a multitude of various other streams exist. Consider for instance AudioInputStream, which reads in audio-based data (sample frames). Or ZipOutputStream, which implements an output stream for writing ZIP (compressed) files. The latter is a subclass of theFilterOutputStream, which acts as a superclass of other transforming output stream classes as well.

Most of these other streams subclass InputStream and OutputStream, i.e. byte streams, which you have seen before. Browse through the Java API docs in order to get an overview of all the stream types included in the standard Java API.

SCANNERS

Earlier, you saw how you can perform advanced output formatting using the format method of the PrintWriter and PrintStream stream classes. This might have left you wondering: what about input? If a text file is formatted in a known manner, how can you easily read out all the data?

In fact, you’ve already seen a few ways to tackle this problem. If you don’t mind your input data being binary, you can use data or object streams to read in the various data types correctly. If your data is given as text, you have seen how the BufferedReader class can help read the data line by line. But what if a line of text contains different fragments you want to parse out?

Luckily, Java provides a simple means to break down input into various fragments—or tokens, as they are called—and translate them according to their data type. The class that helps you do this is the Scanner, found under java.util.Scanner. The following Try It Out shows how it works.

TRY IT OUT Scanning a Grocery List with Prices

In this Try It Out, you use the Scanner class to read in text fragments and translate them according to their data type.

1. You will continue working in the same project as before. Create a new text file next to groceries.txt called grocerieswithprices.txt with the following content:

2. apples, 5.33

3. bananas, 4.61

4. water, 1.00

5. orange juice, 2.50

6. milk, 3.20

bread, 1.11

7. Create a new class called ShowGroceries. First of all, you will take a look at how you would read out the grocery list and show it without using a Scanner:

8. import java.io.BufferedReader;

9. import java.io.FileReader;

10. import java.io.IOException;

11.

12. public class ShowGroceries {

13. public static void main(String[] args) {

14. try (

15. BufferedReader in = new BufferedReader(new FileReader(

16. "grocerieswithprices.txt"));

17. ) {

18. String line;

19. while ((line = in.readLine()) != null) {

20. String[] splittedLine = line.split(", ");

21. String item = splittedLine[0].trim();

22. double price = Double.parseDouble(splittedLine[1].trim());

23. System.out.format("Price of %s is: %.2f%n", item, price);

24. }

25. } catch (IOException e) {

26. e.printStackTrace();

27. }

28.

29. }

}

30. This approach works fine for simple cases, but depending on the nature and structure of your input text, you might want to resort to a Scanner instead, as follows:

31. import java.io.BufferedReader;

32. import java.io.FileReader;

33. import java.io.IOException;

34. import java.util.Locale;

35. import java.util.Scanner;

36. import java.util.regex.Pattern;

37.

38. public class ShowGroceries {

39. public static void main(String[] args) {

40. try (

41. Scanner sc = new Scanner(

42. new FileReader("grocerieswithprices.txt"));

43. ) {

44. sc.useDelimiter(Pattern.compile("(, )|(\r\n)"));

45. sc.useLocale(Locale.ENGLISH);

46. while (sc.hasNext()) {

47. String item = sc.next();

48. double price = sc.nextDouble();

49. System.out.format("Price of %s is: %.2f%n", item, price);

50. }

51. } catch (IOException e) {

52. e.printStackTrace();

53. }

54.

55. }

}

How It Works

Here’s how it works:

1. The Scanner class wraps around a BufferedReader in this example, but can also be directly applied to a string if so desired.

2. Next, we set the delimiter pattern of the Scanner. A delimiter is a piece of string by which we want to split up text fragments. By default, the delimiter of a Scanner equals all white-space characters (newline, a space, a tab), but in our case, we change it to match either a comma followed by a space (“, “) or a newline (\r\n).

3. We also set the Locale of the Scanner to English to make sure the decimal prices are read in and interpreted correctly. Locale is a Java provided class to represent geographical regions. Instead of instantiating a new object, the class provides a series of static members (themselves Locale objects) that can be accessed and used directly, such as Locale.ENGLISH or Locale.KOREA. Your computer might already use English as a default locale, in which case this line can also be removed.

4. Next, a while loop is iterated over so long as the Scanner is able to feed text fragments. If this is the case, we know we can immediately read in two pieces of fragments at once—the item and the price—using the nextDouble method.

5. Note that we use the Pattern class here to supply a regular expression, a relatively advanced format that specifies text patterns. Normally, you could also supply the delimiter as a single string, in which case you might be tempted to try “, “, but this will not work, as the Scanner will then continue to read text even after the end of a line is encountered, which we do not want in this case. Therefore, as a general recommendation, it is best to either stick to newline delimiters only, or use a delimiter that can be easily expressed as a single string, for instance “, “ and put all the data on the same line.

INPUT AND OUTPUT FROM THE COMMAND-LINE

You’ve seen how to use streams to perform input and output operations, e.g., to read and write data to files. In the following section, we’ll explore file I/O a bit deeper, but we first take a slight detour in order to show off input and output with the command-line.

Although most programs you run on your computer nowadays offer some sort of Graphical User Interface (GUI), this has not always been the case. In the old days of computing, programs oftentimes only offered text-like communication possibilities, showing their output and taking input from a so-called “console,” or command-line. Even in modern operating systems, this command-line interface is still present under the hood. In Windows, for instance, you can try firing up the cmd.exe program to get the command prompt shown in Figure 8.4.

images

Figure 8.4

When you execute Java programs in Eclipse, they will also show their output and take their input from a command-line interface, unless you program in some kind of GUI features. We refer here to the Eclipse console. Refer back to Chapter 3 if you want to know how you can run Java programs from the common Windows command-line.

In every console application, three mechanisms exist to communicate with the user. In Java, these are implemented as the “standard streams,” corresponding to the following:

· System.in: A byte InputStream to take user input (you might have expected this to be a character stream, but for historical reasons, this is not the case)

· System.err: A PrintStream to output error messages

· System.out: A PrintStream to output normal messages

You can use these streams just as you would use normal Java streams, with the difference, however, that you don’t need to open or close them. The following Try It Out shows how it works.

TRY IT OUT Input and Output from the Command-Line

This Try It Out illustrates the basic use of System.in, System.err, and System.out by means of a simple greeter application.

1. Create a single class named ReadName with the following content:

2. import java.io.BufferedReader;

3. import java.io.IOException;

4. import java.io.InputStreamReader;

5.

6. public class ReadName {

7. public static void main(String[] args) {

8. try(

9. BufferedReader reader = new BufferedReader(

10. new InputStreamReader(System.in));

11. ) {

12. System.out.println("What is your name, user?");

13. String name = reader.readLine();

14. if (name.trim().equals(""))

15. throw new IllegalArgumentException();

16. System.out.println("Welcome, " + name);

17. } catch (IllegalArgumentException e) {

18. System.err.println("Error: name cannot be blank!");

19. } catch (IOException e) {

20. e.printStackTrace();

21. }

22.

23. }

}

24.Execute the class and watch what happens in Eclipse’s console. Try to enter your name. What happens when you enter a blank name?

How It Works

Here’s how it works:

1. System.in, System.out, and System.err all act as normal streams as you’ve seen before. Note, however, the use of the InputStreamReader class to make a bridge from System.in (a byte stream) to a BufferedReader, which expects a character stream. Why do we use BufferedReader instead of BufferedInputStream here? Because we want to use the readLine method to read in a complete line of user input. This is one of the valid use cases for InputStreamReader. As an alternative, it is also possible to make use of aScanner as seen above, in which case the class would look like follows:

2. import java.util.Scanner;

3.

4. public class ReadName {

5. public static void main(String[] args) {

6. try(

7. Scanner sc = new Scanner(System.in);

8. ) {

9. System.out.println("What is your name, user?");

10. String name = sc.nextLine();

11. if (name.trim().equals(""))

12. throw new IllegalArgumentException();

13. System.out.println("Welcome, " + name);

14. } catch (IllegalArgumentException e) {

15. System.err.println("Error: name cannot be blank!");

16. }

17.

18. }

}

19.Note also that we define two catch blocks: one to catch I/O errors as we’ve done before, and one to catch our own thrown exception in case the user enters a blank name.

The reason why both normal output and error streams are provided is not native to Java, but is due to historical reasons and standardization of command-line I/O in modern operating systems. Both normal output and error streams are provided to allow users to redirect output to a file, while still showing error messages on the command line. This concept is not native to Java, but is included for historical reasons due to the standardization of command-line I/O in modern operating systems.

An example can help to clarify this. Let’s assume that you want to make a class that accepts lines from System.in and then shows the reversed line on System.out. When a palindrome is given, however (a line that reads the same forward or reversed), we’ll also show a toy “warning” on System.err. When a blank line is given, execution is stopped. The final class looks like this:

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

public class LineReverser {

public static void main(String[] args) {

try(

BufferedReader reader = new BufferedReader(

new InputStreamReader(System.in));

) {

String line = null;

while (true) {

line = reader.readLine();

if (line == null || "".equals(line))

break;

String reverse = new StringBuilder(line).reverse().toString();

System.out.println(reverse);

if (line.equals(reverse))

System.err.format("The string '%s' is a palindrome!%n", line);

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

Take some time to execute this class and familiarize yourself with the code. Try to enter a palindrome such as “amoreroma” and see what happens.

Now, we will take this a step further. Right-click on the LineReverser class in Eclipse and choose Export. Next, select Runnable JAR File. See Figure 8.5.

images

Figure 8.5

In the next screen, select LineReverser as the “launch configuration” (this requires that you ran the class in Eclipse at least once). Set the Export Destination to linereverser.jar on your desktop.

Once this is done, open a command-line window (start cmd.exe on Windows) and navigate to your Desktop folder. Usually, typing cd %HOMEPATH%\Desktop does the trick, but if not, navigate to your Desktop folder by using a combination of cd .. and cd foldernamecommands, as shown in Figure 8.6.

images

Figure 8.6

Next, on the command-line, start your Java program using the command java -jar linereverser.jar. The program will start and you will be able to interact with it just as you did from Eclipse’s console. See Figure 8.7.

images

Figure 8.7

Note how both the standard output and standard error streams are shown on the command-line by default. However, we can perform a neat little trick now by what is called “stream redirection.” Let’s say we want to save our reversed lines to a text file, but we do not want to save the error messages. In this case, we can execute the following command: java -jar linereverser.jar > output.txt. See Figure 8.8.

images

Figure 8.8

Note how the error messages are still shown on the console, whereas standard output is now saved in the output.txt text file (verify this by opening the file). Is there a way to redirect error messages as well? Indeed there is, by using 2>: java -jar linereverser.jar > output.txt 2> error.txt. Note that > (standard output redirection) can then also be written as 1>. If you want to append to the end of a file instead of creating a new one, you can use >> (two arrows) instead of >. Try this out if you like.

Let us now take this a step further. Create a text file called input.txt on your Desktop containing the following:

· apple

· amoreroma

· test

· aabbaa

No doubt you can see where this is going; we would like to use the contents of this file as input instead of typing the lines ourselves. Again, this is easy, using the < redirection operator, like so: java -jar linereverser.jar < input.txt. See Figure 8.9.

images

Figure 8.9

Indeed, you can also combine these operators to construct something like this: java -jar linereverser.jar < input.txt > output.txt 2> errors.txt.

So far, you’ve been taking input and sending output from and to files, but it is also possible to redirect the standard streams from one program to another program. To illustrate this, we will use a standard Windows command-line program called type. This program just shows the contents of a file on the standard output. Try it out by typing type input.txt. See Figure 8.10.

images

Figure 8.10

Now, how can you send the standard output of the type program to the program? Using > will not work here, as you’re not sending the output to a file. Instead, you can use the | operator, called the pipe, as it pipes the output from one program to another, i.e.: type input.txt | java -jar linereverser.jar. See Figure 8.11.

images

Figure 8.11

Piping and output redirection are two very powerful features of command-line programs. They allow for writing simple, one-task-only programs that can then be combined and chained together in a manner that’s more flexible than what most GUI applications can offer. This aspect forms one of the key reasons why command-line programs remain useful and used in server environments.

Before we end this section, let us return to Eclipse and Java to highlight a more advanced alternative of the standard streams, named the Console. This is a class that lives under java.io.Console and can be accessed through System.console(). This object provides all features the standard streams have, as well as some other features, such as secure input mechanisms (such as for password entry). The Console also provides input and output streams that are character streams. A downside of this technique however is that the Consoleis not available in all operating systems or environments, so the recommended approach remains to use the standard streams, unless you really need a feature Console provides. The following Try It Out shows how to read passwords in a secure manner using theConsole.

TRY IT OUT Advanced Command-Line Interaction Using the Console

In this Try It Out, we will use System.console() to read in a password in a secure manner.

1. You will create a single class named GetPassword with the following content:

2. import java.io.Console;

3. import java.io.IOException;

4.

5. public class GetPassword {

6. public static void main (String args[]) throws IOException {

7. Console c = System.console();

8. if (c == null) {

9. System.err.println("Console object is not available");

10. System.exit(1);

11. }

12.

13. String username = c.readLine("Enter your username: ");

14. char[] password = c.readPassword("Enter your password: ");

15.

16. if (username.equals("admin") && new String(password).equals("swordfish")) {

17. c.writer().println("Access granted");

18. } else {

19. c.writer().println("Oops, didn't recognize you there");

20. }

21. }

22.

}

23.Executing this program in Eclipse’s console will not work, as Eclipse does not provide a so-called “interactive command-line.” You will need to export the class as a Runnable JAR file like you did before. Make sure to select GetPassword as the launch configuration (execute the program in Eclipse if this does not appear in the list).

24.Once you have exported the program, you can test it using a Windows command-line. Note that the characters you type for the password will not appear on the screen. See Figure 8.12.

images

Figure 8.12

How It Works

This class is fairly straightforward, as most methods of System.console() are self-explanatory. The only thing you need to keep in mind is to first test whether you can access the console in your environment by performing a null check. Next, you use thereadLine and readPassword methods to get user input. Note that the latter does not return a string but an array of characters, which you thus convert to a string to perform the username/password check.

We now turn our attention back to files and continue our discussion on working with file I/O in Java in the following section.

INPUT AND OUTPUT FROM FILES

You’ve already seen some example programs that dealt with files in Java. The file copying programs, for instance, illustrated how you can use byte, character, and buffered streams to get input from and send output to files.

However, there’s more to file I/O than just taking and dropping contents from and into files. Files can be checked for existence, deleted, moved, copied, created, can contain metadata of various sorts, and can be organized into directories, which are also traversable in Java. The following sections teach you how to handle all of these things.

Java NIO2 File Input and Output

Recall from the introduction that Java 7 introduced the “NIO2” API to offer a new file system API. Existing legacy file I/O methods remain supported, so that many code samples, books, tutorials, and real-life code still apply “legacy I/O” API features.

Since newer means better in this case, we will start by applying the NIO2 API toward working with files. The central player in NIO2 is the Path type and its little helper class Paths.

The Path Interface

On your computer, files are stored in a way so that they can be easily retrieved and accessed later. On most file systems, files are stored in a hierarchical structure, meaning as a tree. The top of a tree is called a root node (e.g., C:\) under which a hierarchy of folders can be found. Each folder can contain other folders or files.

This implies that all files on your computer can be identified by a unique path of traversal through the tree. For example, on Windows, the “C:\projects\java book\structure outline.txt” path refers to a text file that lives in the folder named java book, which in turn lives in the folder projects.

NOTE On Linux, Unix, and BSD, paths use a different separator character, namely / instead of \. When programming in Java on Windows, you can also use / instead of \. If you want to retrieve the separator in a programmatic manner, you can call the following method, which returns a string:

FileSystems.getDefault().getSeparator();

The example we provided illustrated a so-called “absolute” path, meaning that the path started from the root element in the file system hierarchy (C:\) and worked its way down from there. A path can also be “relative.” Relative paths need to be combined with other paths to access a file. For example, when the current path is “C:\projects\", the relative path “java book\structure outline.txt” would lead you to the file you had before. The relative path overview.txt would resolve to “C:\projects\overview.txt” and the relative path"..\movies\vacation.avi” would resolve to “C:\movies\vacation.avi”. Note the use of "..", which traverses one level up the directory tree. A single dot (.) in a path refers to the current directory.

Finally, on some systems, file systems can also support links, apart from files and folders. A link is a special file that serves as a reference to another file. Operations on these links are automatically redirected to the target location of the link. You don’t need to concern yourself further regarding this aspect, as they are very uncommon on Windows systems and you won’t use them for most purposes.

In Java, the Path type, found under the java.nio.file package, represents a path in the file system. Path objects contain the filename and directory list used to build the path, and can be used to examine and work with files. Creating a path is done by using the getmethod on the Paths class, also under java.nio.file, like so:

Path myPath = Paths.get("C:\\projects\\outline.txt");

NOTE An important thing to note here is that Path is an interface type, whereas Paths is a normal class (albeit a very simple one, without a public constructor). In case you're wondering why the former is an interface, the reason for this is to allow developers of custom file systems to be able to implement (or extend) it.

The usage of the s suffix of Path versus Paths is also in line with other concepts in NIO2, for instance Files (which you'll encounter later on) and FileSystem versus FileSystems. In fact, calling the get method of Paths is a shorthand for:

FileSystems // A static utility class containing methods to create

FileSystem objects

.getDefault() // Get the default file system FileSystem

.getPath("C:\\projects\\outline.txt") // Return a Path

Once you have created a Path object, there are a number of methods you can execute on them to retrieve information about the path. These methods do not require that the file corresponding to the path actually exist:

· String myPath.toString(): Returns the string representation of the Path object. Note that this method will attempt to perform syntactic cleanup.

· Path myPath.getFileName(): Returns the filename or the last element in the Path object.

· Path myPath.getName(int i): Returns the Path element corresponding to the specified index. Note that index 0 does not represent the root, but the element closest to the root.

· int myPath.getNameCount(): Returns the number of elements in the path.

· Path myPath.subpath(int i, int j): Returns the subsequence of the Path (not including a root element) as specified by beginning and ending indices.

· Path myPath.getParent(): Returns the Path of the parent directory of this path.

· Path myPath.getRoot(): Returns the root of the path.

· Path myPath.normalize(): Cleans up redundancies from a path and returns the cleaned-up result. For example, “C:\.\projects\..\movies\vacation.avi” is converted to “C:\movies\vacation.avi”.

· Path myPath.resolve(String partialPath): The partial path (not including a root element) is added to the original path and the new path is returned. If you pass in an absolute path, the absolute Path itself will be returned.

· Path myPath.relativize(Path otherPath): Constructs a new Path object originating from the original path and ending at the location specified by otherPath. This returns a relative Path.

In addition, methods exist to convert a path:

· URI myPath.toUri(): Converts the path to a string that can be opened by web browsers.

· Path myPath.toAbsolutePath(): Converts a relative path to an absolute one.

· Path myPath.toRealPath(LinkOption... options): Returns the real path of an existing file. If true is passed in the options parameters, links will be resolved to their real paths (if the file system supports links). In addition, relative paths will be converted to absolute ones and redundant elements will be removed.

The Files Class

Apart from the Path interface, the Files class is the other most important class contained in the java.nio.file package. This utility class offers a set of static methods for reading, writing, and manipulating files and folders (do not be thrown off by the fact that the class itself is named Files and not FilesAndFolders).

NOTE Similarly to Path with its Paths helper and FileSystem with its FileSystems counterpart, you might expect Files to contain methods that return File objects. However, the File class already existed before the advent of NIO2 (you'll meet it in the legacy file section ahead), so that all methods in the Files class that do return an object representing an entity in a file system will return it as a Path object, not File.

Checking Existence

The first and most basic operation you can execute using the Files class is performing checks on files and folders. Let’s say you have created a Path object representing a file or folder. How do you check whether this path actually exists? Two methods exist (no pun intended) to do so:

· boolean Files.exists(Path pathToCheck, LinkOption... options)

· boolean Files.notExists(Path pathToCheck, LinkOption... options)

Ignore the options parameter for now. As we’ve said, this parameter is there to specify how links should be dealt with. A more interesting question is why two methods are provided. Couldn’t you just use !Files.exists(path) instead of Files.notExists(path)? The reason for this is because—when checking the existence of a path—three results can occur: the file exists, the file does not exist, or your program cannot determine the existence, for instance, when access rules block your program from reaching the path. If both existsand notExists return false, this means the existence of the path cannot be verified. If exists returns true, this means you can safely continue working with this file, as it exists and can be accessed from your program.

There are also a number of other methods to check a file’s status:

· boolean Files.isReadable(Path pathToCheck): Tests whether a path is readable.

· boolean Files.isWritable(Path pathToCheck): Tests whether a path is writable.

· boolean Files.isExecutable(Path pathToCheck): Tests whether a path is executable.

· boolean Files.isDirectory(Path pathToCheck): Tests whether the path represents a directory.

· boolean Files.isSameFile(Path firstPath, Path secondPath): Tests whether two paths resolve to the same location, taking into account redundant syntax and links.

NOTE Note that the result of performing a check on a Path is atomic and might be violated almost immediately after you continue in your code. Meaning that a file might be deleted, for instance, after you perform an existence check and move on with the rest of your code. This aspect can lead to a particular kind of software bug called TOCTTOU (time of check to time of use) and can also lead to security problems in serious cases. Therefore, never assume too strongly that the result of your check will remain absolutely true throughout the execution of your program, and always be ready to handle exceptions thrown by the Files methods in a graceful manner.

Deleting Files and Folders

The next operation you can perform using the Files class is a bit more volatile, i.e. deletion. It is possible to delete both files and folders, but for folders, the folder needs to be empty before it can be deleted, otherwise an exception will be thrown. The following code snippet shows how the delete method works:

try {

Files.delete(path);

} catch (NoSuchFileException x) {

// File does not exist

} catch (DirectoryNotEmptyException x) {

// The directory is not empty

} catch (IOException x) {

// File permission problem, no access

}

Note that there is also a Files.deleteIfExists(Path pathToDelete) method. This method works in a similar manner, but will not throw an exception when the file does not exist. It just does nothing in that case.

Copying and Moving Files and Folders

Next up, you look at two methods for copying and moving files:

· Path copy(Path source, Path target, CopyOptions... options): Copies a source file to a target file. The options specify how the copy is performed. By default, the copy operation will fail if the target file already exists (except when both are the same file), unless you pass REPLACE_EXISTING as an option. If the file to copy is a directory, an empty directory is created in the target location, but the entries in the directory are not copied. You will see later how to easily copy over a complete directory using this method.

· Path move(Path source, Path target, CopyOptions... options): Moves a source file to a target file. The options specify how the move is performed. By default, the move operation will fail if the target file already exists (except when both are the same file), unless you pass REPLACE_EXISTING as an option. If the file to copy is a directory, the move will be successful if the directory is empty. Otherwise, the result of the move depends on the underlying file system and can throw an IOException in some cases. In that case, a copy should be performed first followed by a manual cleanup of the original source directory.

NOTE Note that these methods provide a more straightforward means to copy and move files than the stream-based file copy programs you've seen before. Again, one of the nice improvements of the NIO2 API.

Reading, Writing, and Creating Files

Next, let’s take a look at how to read, write, and create files using the NIO2 API. In this chapter, you’ve already seen how to use stream-based methods to do this, and the NIO2 API will build on the concept of streams, while also making our lives easier with some helpful methods.

First of all, when you just want to get all bytes or lines from a file, you can use the following methods:

· byte[] Files.readAllBytes(Path path): Reads all the bytes from a file to a byte array. This method takes care of opening and closing the file for you.

· List<String> readAllLines(Path path, Charset cs): Reads all lines from a file to a list of strings. This method takes care of opening and closing the file for you. The Charset parameter specifies which character set should be used for decoding the file. UsingCharset.defaultCharset() or Charset.forName("UTF-8") works best in most cases.

Note, however, that these methods will read the complete contents of a file at once, and thus will fail to work when you are dealing with large files. For small files, however, this approach is straightforward and works just fine. As an example, recall the grocery-reading application we showed off in the introduction. Note that you can completely avoid dealing with streams thanks to the Files class:

import java.io.IOException;

import java.nio.charset.Charset;

import java.nio.file.Files;

import java.nio.file.Paths;

import java.util.ArrayList;

import java.util.List;

public class ShowGroceries {

public static void main(String[] args) {

List<String> groceries = new ArrayList<>();

try {

groceries = Files.readAllLines(

Paths.get("groceries.txt"),

Charset.defaultCharset());

} catch (IOException e) {

e.printStackTrace();

}

for (String item : groceries) {

System.out.println("Don't forget to pickup: " + item);

}

}

}

Similarly, two methods exist to write all bytes or lines to a file:

· Path write(Path path, byte[] bytes, OpenOption... options): Writes bytes to a file. By default, this method creates a new file or overwrites an existing file.

· Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options): Writes lines of text to a file. By default, this method creates a new file or overwrites an existing file. Don’t be confused by the signature of the lines argument. In practical cases, this will work with a List<String>, for instance. The Charset parameter specifies which character set should be used for encoding the file. Using Charset.defaultCharset() or Charset.forName("UTF-8") works best in most cases.

You might be wondering what the OpenOptions parameters are for in these methods. These options are used in various methods and will tell the NIO2 API how you want to open the file. You can pass in the following StandardOpenOptions values:

· WRITE: Opens a file for write access.

· APPEND: Appends new data to the end of the file (use together with WRITE or CREATE).

· TRUNCATE_EXISTING: Empties the file before writing (use with WRITE).

· CREATE_NEW: Creates a new file or throws an exception if the file exists.

· CREATE: Opens the file or creates it if it does not exist.

· DELETE_ON_CLOSE: Deletes the file when the handling stream is closed.

· SPARSE, SYNC, DSYNC: A set of advanced options that we do not describe in detail here.

If you’ve been paying attention, you’ll notice that these options strongly resemble the file operation modes introduced in the beginning of this chapter. In most cases, there’s no need to provide any options at all, however, as the API will assume sensible defaults when you leave them out (CREATE, TRUNCATE_EXISTING, and WRITE for the write methods, for instance). When you do supply your own options, take care not to supply an infeasible combination or a combination that does not match the operation you’re trying to perform with the method you’re calling.

The methods mentioned so far are fine when you’re dealing with small files, but when you need to work with large files, you cannot store the contents of the file in memory all at the same time. We already know how we would approach this problem using buffered character streams:

Reader fr = new FileReader("groceries.txt");

BufferedReader br = new BufferedReader(fr);

However, the NIO2 API provides a cleaner way to read and write files (note for instance that we hardcoded the filename in the code snippet) using the following two methods:

· BufferedReader Files.newBufferedReader(Path path, Charset cs): Returns a buffered character stream to read a text file in an efficient manner using the given character set to decode.

· BufferedReader Files.newBufferedReader(Path path): Same as above, but using UTF-8 as the character set.

· BufferedWriter Files.newBufferedWriter(Path path, Charset cs, OpenOption... options): Returns a buffered character stream to write a text file in an efficient manner using the given character set to encode. If no options are provided, CREATE, TRUNCATE_EXISTING andWRITE are used.

· BufferedWriter Files.newBufferedWriter(Path path, OpenOption... options): Same as above, but using UTF-8 as the character set.

Note how these neatly work together with the Files and Path types. Once called, you can just use the BufferedReader and BufferedWriter streams as you’ve seen before. The following code fragment shows an example using a try-with-resources block, which remains the recommended approach:

Path file = Paths.get("groceries.txt");

try (BufferedReader reader = Files.newBufferedReader(file)) {

String line = null;

while ((line = reader.readLine()) != null) {

System.out.println(line);

}

} catch (IOException x) {

System.err.println("Something went wrong");

}

Next, if you do not want to use buffered character streams but want to use byte streams instead, you’ll need to use the newInputStream and newOutputStream methods. They work similarly to the previous methods, but return InputStream and OutputStream object respectively, which you can then wrap in BufferedInputStream and BufferedOutputStream objects if you want to obtain a buffered byte stream.

NOTE You might be wondering why the character stream variant of these methods return a buffered stream, whereas the byte stream ones return a non-buffered one. This is mainly due to convention: in cases where you will use a character stream, using a buffered approach almost always make sense. You can still obtain an unbuffered character stream if you really need it by using the InputStreamReader and OutputStreamWriter classes and wrapping them around the InputStream andOutputStream objects you get with the newInputStream and newOutputStream methods.

Note that the Java NIO2 API also provides a different method to read and write files, named channel I/O, as an alternative to stream-based I/O. Whereas streams read one character or byte at a time, channels can read or write buffers at a time (and thus avoid the use of a separate buffering mechanism). In addition, channels exist that allow for more fine-grained seeking within a file through a concept called Random Access Files, which permit non-sequential access to files and which allow you to map the contents of a file directly to computer memory. We mention it here for the sake of completeness, but in most “normal” file I/O environments, stream-based I/O works just fine.

Lastly, we can take a look at the methods to create files. While you might just use one of the earlier writing functions to open a new file and immediately close it, it is much cleaner to use the Files.createFile(Path path) method to create an empty file. Note that this method will—by default—throw a permission if a path exists, contrary to the writing methods shown previously, which will overwrite a file if it exists. It is thus a good idea to use this method as an extra fail-safe in cases where this matters. You can also use another method—Files.createTempFile(Path folder, String prefix, String suffix—to create temporary files in the specified folder, using a given prefix, suffix, and a randomized body as a name. Note that you can leave the folder argument out, in which case the standard temporary-file directory provided by your operating system will be used. These methods are helpful to create quick “throwaway” files.

Reading and Creating Folders

Some path operation methods, such as deletion or copying, can work on files and folders. But directories also require an additional set of methods. For example, how would you list all the contents of a folder?

First of all, though, let’s quickly take a look at creating folders. This can be done using the Files.createDirectory(Path path) method or the createTempDirectory(Path folder, String prefix) method in case you want to create a temporary folder (again, the folderargument can be left out).

You list all the contents of a folder using the Files.newDirectoryStream(Path folder) method. Note that this method returns an object that implements the DirectoryStream and Iterable interfaces, so you can loop over this object to read all of the entries. However, since this object is a stream, don’t forget to close it in your finally block (or use a try-with-resources block as we’ve been recommending so far). The following example class shows how to list the contents of a directory:

import java.io.IOException;

import java.nio.file.DirectoryIteratorException;

import java.nio.file.DirectoryStream;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

public class ShowDirectory {

public static void main(String[] args) {

Path folder = Paths.get("C:\\");

try (DirectoryStream<Path> stream = Files.newDirectoryStream(folder)) {

for (Path entry: stream) {

System.out.println(entry.getFileName());

}

} catch (IOException | DirectoryIteratorException x) {

System.err.println("An error occurred");

}

}

}

Example output:

$Recycle.Bin

BOOTNXT

Documents and Settings

eclipse

hiberfil.sys

MSOCache

pagefile.sys

PerfLogs

Program Files

Program Files (x86)

ProgramData

swapfile.sys

System Volume Information

temp

Users

Windows

Note that this approach returns all the contents of a directory: files, subdirectories, hidden files, and links. If you only need a subset, you can just add checks to the for loop using methods you’ve seen before. Another way to filter a directory listing is by using a concept called globbing, using the Files.newDirectoryStream(Path folder, String glob) method. A glob is a special kind of syntax to specify pattern-matching behavior. An asterisk (*), for instance, matches any number of characters; a question mark (?) matches exactly one character. Braces ({one,two}) specify a collection of subpatterns, whereas square patterns ([qwerty]) convey a set or range ([0-9]) of characters. The following method call, for example, only returns DOC, PDF, and TXT files:

Files.newDirectoryStream(dir, "*.{txt,doc,pdf}"));

If you only want to retrieve directories, you’ll need to write your own filter. To do so, you can create a class implementing the DirectoryStream.Filter<T> interface (and implementing the accept method) and use this to invoke the Files.newDirectoryStream(Path folder, DirectoryStream.Filter filter) method. Just using an if (Files.isDirectory(entry)) continue; line might be easier in this case, however.

Recursing Folders

The last operation we’ll take a closer look at involves recursing over the folder tree. When discussing how to copy, move, or delete paths, we’ve stated that copying folders just creates an empty folder, moving folders can fail in some cases, and deleting folders only works when the folder is empty. In addition, you might be interested in writing a method that looks for a file or directory in a directory as well as subdirectories.

For all these tasks, interfaces have been defined by the NIO2 API to do this. The reality, however, is that implementing all interfaces and setting up everything can be a bit daunting for newcomers. For instance, to walk over a directory tree, you’ll need to implementFileVisitor<Path> with the preVisitDirectory, visitFile, postVisitDirectory, and visitFileFailed methods. Look up the Java API docs if you’re interested to learn more.

Instead, the following Try It Out will present a set of alternative recursive methods you can use to copy, move, delete, and search through directories, which you can use as a starting point in your own code as well.

TRY IT OUT Using Recursive Operations

In this Try It Out, you implement a set of methods to search, delete, copy, and move complete directories at once.

1. In Eclipse, create a class called RecursiveOperations.

2. First, add a method to delete files:

3. public static void delete(Path source) throws IOException {

4. if (Files.isDirectory(source)) {

5. for (Path file : getFiles(source))

6. delete(file);

7. }

8. Files.delete(source);

9. System.out.println("DELETED "+source.toString());

}

10. Next, you need to provide the getFiles method. This helper method returns all files and subdirectories in a given folder. However, since deletion will only work on empty folders, you want this method to first return all directories, followed by all files to ensure that the delete method first goes through all directories as far as possible:

11. public static List<Path> getFiles(Path dir) {

12. // Gets all files, but puts directories first

13. List<Path> files = new ArrayList<>();

14. if (!Files.isDirectory(dir))

15. return files;

16. try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {

17. for (Path entry : stream)

18. if (Files.isDirectory(entry))

19. files.add(entry);

20. } catch (IOException | DirectoryIteratorException x) {

21. }

22. try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {

23. for (Path entry : stream)

24. if (!Files.isDirectory(entry))

25. files.add(entry);

26. } catch (IOException | DirectoryIteratorException x) {

27. }

28. return files;

}

29. Next up, you can define a copy method in a similar fashion:

30. public static void copy(Path source, Path target) throws IOException {

31. if (Files.exists(target) && Files.isSameFile(source, target))

32. return;

33. if (Files.isDirectory(source)) {

34. Files.createDirectory(target);

35. System.out.println("CREATED "+target.toString());

36. for (Path file : getFiles(source))

37. copy(file, target.resolve(file.getFileName()));

38. } else {

39. Files.copy(source, target);

40. System.out.println(

41. "COPIED "+source.toString()+" -> "+target.toString());

42. }

}

43. The move method just reuses the copy and delete methods:

44. public static void move(Path source, Path target) throws IOException {

45. if (Files.exists(target) && Files.isSameFile(source, target))

46. return;

47. copy(source, target);

48. delete(source);

}

49. The search method is added as follows, and uses a PathMatcher object to match a filename to a provided glob:

50. public static Set<Path> search(Path start, String glob,

51. boolean includeDirectories, boolean includeFiles) {

52.

53. PathMatcher matcher = FileSystems.getDefault().getPathMatcher

54. ("glob:" + glob);

55. Set<Path> results = new HashSet<>();

56. search(start, matcher, includeDirectories, includeFiles, results);

57. return results;

58. }

59.

60. private static void search(Path path, PathMatcher matcher,

61. boolean includeDirectories, boolean includeFiles, Set<Path> results) {

62.

63. if (matcher.matches(path.getFileName())

64. && ((includeDirectories && Files.isDirectory(path))

65. || (includeFiles && !Files.isDirectory(path)))) {

66. results.add(path);

67. }

68.

69. for (Path next : getFiles(path))

70. search(next, matcher, includeDirectories, includeFiles, results);

}

71. Finally, you can add a main class to test these methods:

72. public static void main(String args[]) throws IOException {

73. // WARNING: TAKE CARE WHEN TESTING THESE FUNCTIONS ON

74. // EXISTING FOLDERS ON YOUR SYSTEM

75.

76. // Set up test directory

77. try {

78. delete(Paths.get("C:\\javatest\\"));

79. delete(Paths.get("C:\\javatest2\\"));

80. } catch(NoSuchFileException e) {}

81. Files.createDirectory(Paths.get("C:\\javatest\\"));

82. Files.createDirectory(Paths.get("C:\\javatest\\subdir\\"));

83. Files.createFile(Paths.get("C:\\javatest\\text1.txt"));

84. Files.createFile(Paths.get("C:\\javatest\\text2.txt"));

85. Files.createFile(Paths.get("C:\\javatest\\other.txt"));

86. Files.createFile(Paths.get("C:\\javatest\\subdir\\text3.txt"));

87. Files.createFile(Paths.get("C:\\javatest\\subdir\\other.txt"));

88.

89. // Test our methods

90. copy(Paths.get("C:\\javatest\\subdir\\"),

91. Paths.get("C:\\javatest\\subdircopy\\"));

92. System.out.println(search(Paths.get("C:\\javatest"),

93. "text*.txt", true, true));

94. move(Paths.get("C:\\javatest\\subdircopy\\"),

95. Paths.get("C:\\javatest\\subdircopy2\\"));

96. System.out.println(search(Paths.get("C:\\javatest\\"),

97. "text*.txt", true, true));

98. copy(Paths.get("C:\\javatest\\"), Paths.get("C:\\javatest2\\"));

99.

}

100. Executing this code yields the following output:

101. CREATED C:\javatest\subdircopy

102. COPIED C:\javatest\subdir\other.txt -> C:\javatest\subdircopy\other.txt

103. COPIED C:\javatest\subdir\text3.txt -> C:\javatest\subdircopy\text3.txt

104. [C:\javatest\subdir\text3.txt, C:\javatest\text2.txt,

105. C:\javatest\subdircopy\text3.txt, C:\javatest\text1.txt]

106. CREATED C:\javatest\subdircopy2

107. COPIED C:\javatest\subdircopy\other.txt -> C:\javatest\subdircopy2\other.txt

108. COPIED C:\javatest\subdircopy\text3.txt -> C:\javatest\subdircopy2\text3.txt

109. DELETED C:\javatest\subdircopy\other.txt

110. DELETED C:\javatest\subdircopy\text3.txt

111. DELETED C:\javatest\subdircopy

112. [C:\javatest\subdir\text3.txt, C:\javatest\subdircopy2\text3.txt,

113. C:\javatest\text2.txt, C:\javatest\text1.txt]

114. CREATED C:\javatest2

115. CREATED C:\javatest2\subdir

116. COPIED C:\javatest\subdir\other.txt -> C:\javatest2\subdir\other.txt

117. COPIED C:\javatest\subdir\text3.txt -> C:\javatest2\subdir\text3.txt

118. CREATED C:\javatest2\subdircopy2

119. COPIED C:\javatest\subdircopy2\other.txt -> C:\javatest2\subdircopy2\other.txt

120. COPIED C:\javatest\subdircopy2\text3.txt -> C:\javatest2\subdircopy2\text3.txt

121. COPIED C:\javatest\other.txt -> C:\javatest2\other.txt

122. COPIED C:\javatest\text1.txt -> C:\javatest2\text1.txt

COPIED C:\javatest\text2.txt -> C:\javatest2\text2.txt

How It Works

Here’s how it works:

1. Although the class is relatively large, most of the operations we’ve included follow the same general reasoning: loop over subdirectories until you cannot go any further, then perform operations on the included files.

2. Two important notes to keep in mind are that the getFiles method first returns a list of directories, followed by normal files, so that deletion can first clean up the deepest directory before working its way up. Another interesting aspect is how we use the PathMatcher class to match a filename to a glob. The following code snippet can come in handy in other scenarios as well:

3. Path path = ...;

4. PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);

5. if (matcher.matches(path.getFileName())

// do something

6. The “glob:" prefix in the getPathMatcher method should be provided by convention to indicate your pattern is a glob type.

7. We’ve added a check to prevent copying and moving a source to the same target. What would happen if you tried to move a directory under one of its subdirectories? Can you imagine ways to prevent this (or deal with this case)?

8. If you’re interested in knowing how to copy directories using the visitor pattern offered by the NIO2 API, you can take a look at this example online: http://docs.oracle.com/javase/tutorial/essential/io/examples/Copy.java.

Other Methods

Finally, there are a number of additional methods in the Files class that can come in handy. First, metadata methods such as Files.size(Path p)(returns the size of the file in bytes), Files.getOwner(Path p, LinkOption... options), and Files.getLastModifiedTime(Path p, LinkOption... options) can be used to retrieve (and set) metadata. Secondly, functionality exists to watch a file for changes. Look up NIO’s WatchService if you ever need this feature. Lastly, we briefly touched on the concept of link. The NIO2 API also provides functionality to create and modify such links in the file system, but we skipped an in-depth discussion and refer interested intermediate readers to other sources, such as the Java API docs.

Legacy File Input and Output

Prior to the Java 7 release, the java.io.File class was the default mechanism used for file I/O. This class is still present for reasons of backward-compatibility, but has several drawbacks:

· Some methods don’t throw exceptions when an error occurs, or do not provide enough information to know the root cause behind a failure.

· Well-defined support for links is lacking.

· Accessing file metadata can be difficult and slow.

· Fetching information over a network introduces scalability issues.

· Some methods do not work consistently on various operating systems and platforms.

That being said, a great deal of real-life code still uses the legacy file input and output, so we discuss it here in brief. Creating a file object is simple:

File myFile = new File("groceries.txt");

Whenever you can, a good way to deal with methods returning legacy File objects is to immediately convert them to a Path using the myFile.toPath() method. Similarly, the Path interface defines a toFile() method you can use to get a legacy File object from a Pathobject.

Operations you wish to execute on a File object (such as checking for existence) are not done through a helper class like Files did on Path objects, but directly on the File object itself, for example by calling one of its methods. The following list shows the corresponding File methods for most of the Files equivalents we’ve discussed:

· Path.getFileName(...) was File.getName()

· Files.isDirectory(...) was File.isDirectory(...)

· Files.isRegularFile(...) was File.isFile(...)

· Files.size(...) was File.length(...)

· Files.move(...) was File.renameTo(...)

· Files.delete(...) was File.delete(...)

· Files.createFile(...) was File.createNewFile(...)

· Files.createTempFile(...) was File.createTempFile(...)

· File.deleteOnExit(...) is now handled by passing DELETE_ON_CLOSE as an option to Files.createFile(...)

· Files.exists(...) and Files.notExists(...) was File.exists(...)

· Path.newDirectoryStream(...) was File.list(...) and File.listFiles(...)

· Path.createDirectory(...) was File.mkdir(...)

As an example, the following code fragment shows how to loop over the contents of a directory using the legacy API:

import java.io.File;

public class ShowDirectory {

public static void main(String[] args) {

File folder = new File("C:\\");

for (File entry : folder.listFiles()) {

System.out.println(entry.getName());

}

}

}

This particular example might appear simple compared to the corresponding NIO2 implementation (fewer imports are used), but keep in mind the other advantages of NIO2 (reading and writing, for instance). The NIO2 approach remains the recommended one.

A Word on FileUtils

Java’s file input/output classes—and the legacy ones in particular—miss some widely used features that are both somewhat annoying and time consuming to implement yourself, or require a great amount of exception juggling to implement gracefully. You’ve already seen an example of this in the form of the implementation of copy, move, and delete operations for whole directories.

When you’re looking for an implementation to properly copy, clean, and move directories or perform many other common file operations, the FileUtils utility class by the Apache Commons project is worth looking at. In fact, many programmers consider this class so helpful and so essential that they will include it as a library in any new Java project they set up. Take a look at the following website if you’re interested in knowing more or want to download and use the library (add it to Eclipse’s build path in order to use it):http://commons.apache.org/proper/commons-io/.

One downside of the FileUtils library, however, is that it is built with Java 6 compatibility in mind, meaning that the legacy File class is used in its method arguments, without Path and Files being present. If you use the FileUtils library, consider using the previously discussed Path.toFile() method to keep the rest of your code base NIO-ready.

CONCLUSION

This concludes this chapter on basic input/output with Java and file input/output. You’ve seen what is meant by stream-based input/output, learned how to interact with users over the command-line, and learned how to write and read content to and from files, using both the NIO2 and legacy API. As always, don’t be afraid to peruse the Java docs or explore the methods offered by Path and Files and other classes using Eclipse’s autosuggest functionality. It is a great way to experiment and learn.

This chapter was largely about saving and reading data to and from files. The next chapter introduces databases, a more advanced and powerful method to store and retrieve information.