Java 7 Features That You May Have Missed - Java SE 8 for the Really Impatient (2014)

Java SE 8 for the Really Impatient (2014)

Chapter 9. Java 7 Features That You May Have Missed

Topics in This Chapter

Image 9.1 Exception Handling Changes

Image 9.2 Working with Files

Image 9.3 Implementing the equals, hashCode, and compareTo Methods

Image 9.4 Security Requirements

Image 9.5 Miscellaneous Changes

Image Exercises

When Java 7 was released, most reviewers focused on the new language features: strings in switch statements, binary literals, underscores in literals, improved type inference, and so on. In this chapter, I will write about some of the library changes that haven’t been discussed so much and that I have found far more useful in daily work than switching on strings or binary literals. I cover one language change that is very useful in daily work—the try-with-resources statement.

The key points of this chapter are:

• Use the try-with-resources statement with any object that implements AutoCloseable.

• The try-with-resources statement rethrows the primary exception if closing a resource throws another exception.

• You can catch unrelated exceptions with a single catch clause.

• The exceptions for reflective operations now have a common superclass ReflectiveOperationException.

• Use the Path interface instead of the File class.

• You can read and write all characters, or all lines, of a text file with a single command.

• The Files class has static methods for copying, moving, and deleting files, and for creating files and directories.

• Use Objects.equals for null-safe equality testing.

• Objects.hash makes it simple to implement the hashCode method.

• When comparing numbers in a comparator, use the static compare method.

• Applets and Java Web Start applications continue to be supported in corporate environments, but they may no longer be viable for home users.

• Everyone’s favorite trivial change: "+1" can now be converted to an integer without throwing an exception.

• Changes in ProcessBuilder make it simple to redirect standard input, output, and error streams.

9.1. Exception Handling Changes

I start this chapter with the Java 7 features for exception handling, since they have a major impact on writing reliable programs. I briefly review the try-with-resources statement before moving on to more subtle changes.

9.1.1. The try-with-resources Statement

Java 7 provides a useful shortcut to the code pattern

open a resource
try {
work with the resource
}
finally {
close the resource
}

provided the resource belongs to a class that implements the AutoCloseable interface. That interface has a single method

void close() throws Exception


Image NOTE

There is also a Closeable interface. It is a subinterface of AutoCloseable, also with a single close method, but that method is declared to throw an IOException.


In its simplest variant, the try-with-resources statement has the form

try (Resource res = ...) {
work with res
}

When the try block exits, res.close() is called automatically. Here is a typical example—reading all words of a file:

try (Scanner in = new Scanner(Paths.get("/usr/share/dict/words"))) {
while (in.hasNext())
System.out.println(in.next().toLowerCase());
}

When the block exits normally, or when there is an exception, the in.close() method is called, exactly as if you had used a finally block.

You can specify multiple resources, for example:

try (Scanner in = new Scanner(Paths.get("/usr/share/dict/words"));
PrintWriter out = new PrintWriter("/tmp/out.txt")) {
while (in.hasNext())
out.println(in.next().toLowerCase());
}

No matter how the block exits, both in and out are closed if they were constructed. This was surprisingly difficult to implement correctly prior to Java 7 (see Exercise 1).


Image NOTE

A try-with-resources statement can itself have catch clauses and a finally clause. These are executed after closing the resources. In practice, it’s probably not a good idea to pile so much onto a single try statement.


9.1.2. Suppressed Exceptions

Whenever you work with input or output, there is an awkward problem with closing the resource after an exception. Suppose an IOException occurs and then, when closing the resource, the call to close throws another exception.

Which exception will actually be caught? In Java, an exception thrown in a finally clause discards the previous exception. This sounds inconvenient, and it is. After all, the user is likely to be much more interested in the original exception.

The try-with-resources statement reverses this behavior. When an exception is thrown in a close method of one of the AutoCloseable objects, the original exception gets rethrown, and the exceptions from calling close are caught and attached as “suppressed” exceptions. This is a very useful mechanism that would be tedious to implement by hand (see Exercise 2).

When you catch the primary exception, you can retrieve those secondary exceptions by calling the getSuppressed method:

try {
...
} catch (IOException ex) {
Throwable[] secondaryExceptions = ex.getSuppressed();
}

If you want to implement such a mechanism yourself in the (hopefully rare) situation when you can’t use the try-with-resources statement, call

ex.addSuppressed(secondaryException);


Image NOTE

The classes Throwable, Exception, RuntimeException, and Error have constructors with an option for disabling suppressed exceptions and for disabling stack traces. When suppressed exceptions are disabled, calling addSuppressed has no effect, andgetSuppressed returns a zero-length array. When stack traces are disabled, calls to fillInStackTrace have no effect, and getStackTrace returns a zero-length array. This can be useful for VM errors when memory is low, or for programming languages on the VM that use exceptions to break out of nested method calls.



Image CAUTION

Detecting secondary exceptions only works when it isn’t actively sabotaged. In particular, if you use a Scanner, and if input fails, and then closing fails, the Scanner class catches the input exception, closes the resource and catches that exception, and then throws an entirely different exception, without linking the suppressed exceptions.


9.1.3. Catching Multiple Exceptions

As of Java SE 7, you can catch multiple exception types in the same catch clause. For example, suppose that the action for missing files and unknown hosts is the same. Then you can combine the catch clauses:

try {
Code that might throw exceptions
}
catch (FileNotFoundException | UnknownHostException ex) {
Emergency action for missing files and unknown hosts
}
catch (IOException ex) {
Emergency action for all other I/O problems
}

This feature is only needed when catching exception types that are not subclasses of one another.

Catching multiple exceptions doesn’t just make your code look simpler but is also more efficient. The generated bytecodes contain a single block for the shared catch clause.


Image NOTE

When you catch multiple exceptions, the exception variable is implicitly final. For example, you cannot assign a new value to ex in the body of the clause catch (FileNotFoundException | UnknownHostException ex) { ... }.


9.1.4. Easier Exception Handling for Reflective Methods

In the past, when you called a reflective method, you had to catch multiple unrelated checked exceptions. For example, suppose you construct a class and invoke its main method:

Class.forName(className).getMethod("main").invoke(null, new String[] {});

This statement can cause a ClassNotFoundException, NoSuchMethodException, IllegalAccessException, or InvocationTargetException.

Of course, you can use the feature described in the preceding section and combine them in a single clause:

catch (ClassNotFoundException | NoSuchMethodException
| IllegalAccessException | InvocationTargetException ex) { ... }

However, that is still very tedious. Plainly, it is bad design not to provide a common superclass for related exceptions. That design flaw has been remedied in Java 7. A new superclass ReflectiveOperationException has been introduced so that you can catch all of these exceptions in a single handler:

catch (ReflectiveOperationException ex) { ... }

9.2. Working with Files

The try-with-resources statement is my favorite feature in Java 7, but the file handling improvements are a close second. Operations that used to be tedious, such as reading a file into a string, or copying a file to another, are now as easy as they should have been all along. These are part of the “NIO2” effort, which refreshes the NIO (“new I/O”) library introduced in 2002 with Java 1.4. (It is never a good idea to include “new” in a product name—what is new today will invariably become old, and the name will look silly.)

Before you can learn how to carry out these easy file operations, you have to learn about the Path interface that replaces the File class. Next, you will see how to read and write files. Finally, we will turn to file and directory operations.

9.2.1. Paths

A Path is a sequence of directory names, optionally followed by a file name. The first component of a path may be a root component, such as / or C:\. The permissible root components depend on the file system. A path that starts with a root component is absolute. Otherwise, it is relative. For example, here we construct an absolute and a relative path. For the absolute path, we assume a computer running a Unix-like file system.

Path absolute = Paths.get("/", "home", "cay");
Path relative = Paths.get("myprog", "conf", "user.properties");

The static Paths.get method receives one or more strings, which it joins with the path separator of the default file system (/ for a Unix-like file system, \ for Windows). It then parses the result, throwing an InvalidPathException if the result is not a valid path in the given file system. The result is a Path object.

You can also provide a string with separators to the Paths.get method:

Path homeDirectory = Paths.get("/home/cay");


Image NOTE

Just like a File object, a Path object does not have to correspond to a file that actually exists. It is merely an abstract sequence of names. To create a file, first make a path, then call a method to create the corresponding file.


It is very common to combine or resolve paths. The call p.resolve(q) returns a path according to these rules:

• If q is absolute, then the result is q.

• Otherwise, the result is “p then q,” according to the rules of the file system.

For example, suppose your application needs to find its configuration file relative to the home directory. Here is how you can combine the paths:

Path configPath = homeDirectory.resolve("myprog/conf/user.properties");
// Same as homeDirectory.resolve(Paths.get("myprog/conf/user.properties"));

There is a convenience method resolveSibling that resolves against a path’s parent, yielding a sibling path. For example, if workPath is /home/cay/myprog/work, the call

Path tempPath = workPath.resolveSibling("temp");

yields /home/cay/myprog/temp.

The opposite of resolve is relativize. The call p.relativize(r) yields the path q which, when resolved with p, yields r. For example,

Paths.get("/home/cay").relativize(Paths.get("/home/fred/myprog"))

yields ../fred/myprog, assuming we have a file system that uses .. to denote the parent directory.

The normalize method removes any redundant . and .. components (or whatever the file system may deem redundant). For example, normalizing the path /home/cay/../fred/./myprog yields /home/fred/myprog.

The toAbsolutePath method yields the absolute path of a given path. If the path is not already absolute, it is resolved against the “user directory”—that is, the directory from which the JVM was invoked. For example, if you launched a program from /home/cay/myprog, thenPaths.get("config").toAbsolutePath() returns /home/cay/myprog/config.

The Path interface has many useful methods for taking paths apart and combining them with other paths. This code sample shows some of the most useful ones:

Path p = Paths.get("/home", "cay", "myprog.properties");
Path parent = p.getParent(); // The path /home/cay
Path file = p.getFileName(); // The last element, myprog.properties
Path root = p.getRoot(); // The initial segment / (null for a relative path)


Image NOTE

Occasionally, you may need to interoperate with legacy APIs that use the File class instead of the Path interface. The Path interface has a toFile method, and the File class has a toPath method.


9.2.2. Reading and Writing Files

The Files class makes quick work of common file operations. For example, you can easily read the entire contents of a file:

byte[] bytes = Files.readAllBytes(path);

If you want to read the file as a string, call readAllBytes followed by

String content = new String(bytes, StandardCharsets.UTF_8);

But if you want the file as a sequence of lines, call

List<String> lines = Files.readAllLines(path);

Conversely, if you want to write a string, call

Files.write(path, content.getBytes(StandardCharsets.UTF_8));

You can also write a collection of lines with

Files.write(path, lines);

To append to a given file, use

Files.write(path, lines, StandardOpenOption.APPEND);


Image NOTE

By default, all methods of the Files class that read or write characters use the UTF-8 encoding. In the (hopefully unlikely) case that you need a different encoding, you can supply a Charset argument. Contrast with the String constructor and getBytes method which use the platform default. Commonly used desktop operating systems still use archaic 8-bit encodings that are incompatibe with UTF-8, so we must specify the encoding whenever converting between strings and bytes.


When you work with text files of moderate length, it is usually simplest to process the contents as a single string or list of strings. If your files are large or binary, you can still use the familiar streams or readers/writers:

InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(path);
Reader reader = Files.newBufferedReader(path);
Writer writer = Files.newBufferedWriter(path);

These convenience methods save you from having to deal with FileInputStream, FileOutputStream, BufferedReader, or BufferedWriter.

Occasionally, you may have an InputStream (for example, from a URL) and you want to save its contents to a file. Use

Files.copy(in, path);

Conversely,

Files.copy(path, out);

copies a file to an output stream.

9.2.3. Creating Files and Directories

To create a new directory, call

Files.createDirectory(path);

All but the last component in the path must already exist. To create intermediate directories as well, use

Files.createDirectories(path);

You can create an empty file with

Files.createFile(path);

The call throws an exception if the file already exists. The check for existence and the creation are atomic. If the file doesn’t exist, it is created before anyone else has a chance to do the same.

The call path.exists() method checks whether the given file or directory exists, but of course it might cease to exist by the time the method has returned.

There are convenience methods for creating a temporary file or directory in a given or system-specific location.

Path newPath = Files.createTempFile(dir, prefix, suffix);
Path newPath = Files.createTempFile(prefix, suffix);
Path newPath = Files.createTempDirectory(dir, prefix);
Path newPath = Files.createTempDirectory(prefix);

Here, dir is a Path, and prefix/suffix are strings which may be null. For example, the call Files.createTempFile(null, ".txt") might return a path such as /tmp/1234405522364837194.txt.


Image NOTE

To read files from a directory, use the Files.list and Files.walk methods described in Chapter 8.


9.2.4. Copying, Moving, and Deleting Files

To copy a file from one location to another, simply call

Files.copy(fromPath, toPath);

To move a file (that is, copy and delete the original), call

Files.move(fromPath, toPath);

You can also use this command to move an empty directory.

The copy or move will fail if the target exists. If you want to overwrite an existing target, use the REPLACE_EXISTING option. If you want to copy all file attributes, use the COPY_ATTRIBUTES option. You can supply both like this:

Files.copy(fromPath, toPath, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);

You can specify that a move should be atomic. Then you are assured that either the move completed successfully, or the source continues to be present. Use the ATOMIC_MOVE option:

Files.move(fromPath, toPath, StandardCopyOption.ATOMIC_MOVE);

Finally, to delete a file, simply call

Files.delete(path);

This method throws an exception if the file doesn’t exist, so instead you may want to use

boolean deleted = Files.deleteIfExists(path);

The deletion methods can also be used to remove an empty directory.


Image NOTE

There is no convenient method for removing or copying a nonempty directory. See the API documentation of the FileVisitor interface for code outlines that achieve these tasks.


9.3. Implementing the equals, hashCode, and compareTo Methods

Java 7 introduces several methods that make it more convenient to deal with null values in the ubiquitous equals and hashCode, and with numeric comparisons in compareTo.

9.3.1. Null-safe Equality Testing

Suppose you have to implement the equals method for this class:

public class Person {
private String first;
private String last;
...
}

First, there is the drudgery of casting the parameter to a Person:

public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (otherObject == null) return false;
if (getClass() != otherObject.getClass()) return false;
Person other = (Person) otherObject;
...
}

Unfortunately, that drudgery isn’t simplified yet. But now it gets better. Instead of worrying that first or last might be null, just call

return Objects.equals(first, other.first) && Objects.equals(last, other.last);

The call Objects.equals(a, b) returns true if both a and b are null, false if only a is null, and a.equals(b) otherwise.


Image NOTE

In general, it is a good idea to call Objects.equals(a, b) when you would have called a.equals(b) before.


9.3.2. Computing Hash Codes

Consider computing a hash code for the preceding class. The Objects.hashCode method returns a code of 0 for a null argument, so you can implement the body of your hashCode method like this:

return 31 * Objects.hashCode(first) + Objects.hashCode(last);

But it gets better than that. The varargs method Objects.hash, introduced in Java 7, lets you specify any sequence of values, and their hash codes get combined:

return Objects.hash(first, last);


Image NOTE

Objects.hash simply calls Arrays.hash, which existed since Java 5. But it isn’t a varargs method, making it less convenient.



Image NOTE

There has always been a null-safe way of calling toString as String.valueOf(obj). If obj is null, the string "null" is returned. If you don’t like that, you can use Object.toString and supply the value to be used for null, for exampleObject.toString(obj, "").


9.3.3. Comparing Numeric Types

When you compare integers in a comparator, it is tempting to return the difference between them since you are allowed to return any negative or positive number—only the sign matters. For example, suppose you are implementing a Point class:

public int compareTo(Point other) {
int diff = x - other.x; // Risk of overflow
if (diff != 0) return diff;
return y - other.y;
}

But that is problematic. If x is large and other.x is negative, the difference can overflow. That makes compareTo rather tedious (see Exercise 8).

As of Java 7, use the static Integer.compare method:

public int compareTo(Point other) {
int diff = Integer.compare(x, other.x); // No risk of overflow
if (diff != 0) return diff;
return Integer.compare(y, other.y);
}

In the past, some people used new Integer(x).compareTo(other.x), but that creates two boxed integers. The static method has int parameters.

The static compare method has also been added to Long, Short, Byte, and Boolean. If you need to compare two char values, you can safely subtract them because the result will not overflow. (The same is true for short or byte, of course.)


Image NOTE

The static compare method existed for Double and Float since Java 1.2.


9.4. Security Requirements

When Java 1.0 was introduced in 1995 to an astounded world, the feature that caught everyone’s imagination were applets: remotely served code that runs inside the user’s web browser. The designers of Java knew perfectly well that executing remote code is a security risk, so they designed a “sandbox” model that stopped any damaging instructions in their tracks.

Soon thereafter, academic researchers found some implementation flaws that were promptly fixed, and other academic researchers groused in general over the fact that the Java security model was rather complex and there was little assurance that its darker corners are safe from assault. At the time, I didn’t take that very seriously because the vast majority of Java applet consumers used Microsoft Windows, which was far less secure and far more complex.

Applets were limited to visual effects and network connections to the originating host, which many application writers found limiting. They wanted local device access for storage, printing, and so on. In 2001, Java Web Start delivered an extension of the sandbox that was quite powerful, comparable to today’s HTML 5 extensions for local device access. Unfortunately, Java Web Start was poorly understood, not integrated with applets, and not maintained with any vigor.

Instead, many application developers simply signed their web-delivered programs, which gave them full permission to do anything on the user’s machine. Signing certificates from commercial entities are within reach of anyone willing to endure some cost and pain. It was also possible to apply a meaningless self-signed certificate, or have users agree to run an applet without a certificate. Warnings were toned down from one release to the next until they became background noise. This was very bad.

Meanwhile, Microsoft, with an enormous engineering effort, got better at closing Windows loopholes, and it became worthwhile for hackers to look at obscure Java vulnerabilities instead. When Oracle purchased Sun in 2010, they inherited a very limited infrastructure for dealing with such attacks and no reliable means of updating client virtual machines. Hackers became increasingly successful in exploiting Java implementation bugs. In this regard, the early researchers who warned of a large attack surface in the Java security model were entirely justified. It took Oracle until 2013 to credibly respond to attacks. Management of client VMs is still a work in progress.

As of today, Oracle signals that it is no longer focused on securing home users’ Java applets and Web Start applications (collectively called rich internet applications, or RIAs). Oracle continues to close Java vulnerabilities, and develops tools that are suitable for corporate deployment, so that legacy RIAs can be deployed safely. From a commercial standpoint, this makes sense. Home users are expected to migrate away from PCs to tablets and smartphones. These devices don’t support a Java VM in the browser. And business users are a plausible revenue target for maintaining legacy applications.

With successive Java 7 releases, Oracle has tightened the security rules. As of January 2014, RIAs running outside the sandbox need to be signed by a commercial certificate authority. Another requirement is designed to thwart “repurposing attacks.” Currently, it is possible to grab a legitimately signed JAR file from a third party and serve it from a hacker site, exploiting some vulnerability in that third-party app. As of January 2014, all JARs must have a manifest entry

Permissions: sandbox

or

Permissions: all-permissions

Since the manifest entry is inside the JAR file, it is signed and cannot be modified afterwards. The Java client will not permit sandbox execution of an all-permission client, which prevents “drive-by” attacks where an applet runs without any user consent. Of course, it is still possible to attack users who are habituated to agree to any security dialogs. To make that harder, another, as yet optional, manifest entry has been added:

Codebase: https://www.mycompany.com www.mycompany.com:8080

only allows the application to be loaded from one of the given URLs.


Image NOTE

It has always been possible to call applets from JavaScript—another dubious decision from the point of view of the security minded. If you are using that feature in your application, you can minimize the repurposing risk by adding an entry Caller-Allowable-Codebase: https://www.mycompany.com to the manifest before signing.


Overall, these developments are very sad. Java held great promise as a universal execution platform for remote code. If Java had offered a more compelling sandbox, if nonsandbox code had been more aggressively controlled, if there had been consistent response to security breaches, and if client VMs had been reliably updated, Java might still be that universal platform. But there is no use dwelling on what might have been. At this point, Java is no longer a viable platform for widespread distribution of client applications over the Internet.

If you maintain an applet or Java Web Start application for home users, the message is clear: Move away from it. If your application serves a specialized audience (for example, software development, image editing, or document processing), make your users install Java or bundle a JVM with your installer. If you target a general audience—such as game players—consider using another technology, perhaps HTML 5.


Image NOTE

If you decide to make your users install Java, you face another hurdle. If you direct Windows users to the installer at http://java.com, they will receive the widely reviled toolbar for Ask.com by default (see Figure 9–1). You have a couple of alternatives. You could have your users install the JDK, directing them to www.oracle.com/technetwork/java/javase/downloads and providing them with instructions for traversing that ever-changing page. Or you could bundle a JVM, which you are then obligated to update since no effective update mechanism is supplied by Oracle.

Image

Figure 9–1 By default, the Windows JRE installer installs the Ask toolbar.


In a corporate environment, you can effectively secure Java RIAs, provided you have control over the applications and the client machines. You will need to tightly manage the application packaging and be ready to update client VMs when security updates become available.


Image NOTE

To more tightly manage corporate RIAs, you can provide deployment rulesets on end-user machines. The process—not for the faint of heart—is explained at http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/deployment_rules.html.


9.5. Miscellaneous Changes

As in the preceding chapter, this section describes a number of smaller features that you may find interesting or useful.

9.5.1. Converting Strings to Numbers

Prior to JDK 1.7, what was the result of the following code segment?

double x = Double.parseDouble("+1.0");
int n = Integer.parseInt("+1");

Pat yourself on the back if you knew the answer: +1.0 has always been a valid floating-point number, but until Java 7, +1 was not a valid integer.

This has now been fixed for all the various methods that construct int, long, short, byte, and BigInteger values from strings. There are more of them than you may think. In addition to parse(Int|Long|Short|Byte), there are decode methods that work with hexadecimal and octal inputs, and valueOf methods that yield wrapper objects. The BigInteger(String) constructor is also updated.

9.5.2. The Global Logger

In order to encourage the use of logging even in simple cases, the Logger class has a global logger instance. It was meant to be as easy as possible to use, so that you could always add trace statements as Logger.global.finest("x=" + x); instead ofSystem.out.println("x=" + x);.

Unfortunately, that instance variable has to be initialized somewhere, and if other logging happens in the static initialization code, it was possible to cause deadlocks. Therefore, Logger.global was deprecated in Java 6. Instead, you were supposed to callLogger.getLogger(Logger.GLOBAL_LOGGER_NAME), which wasn’t anyone’s idea of quick and easy logging.

In Java 7, you can call Logger.getGlobal() instead, which isn’t too bad.

9.5.3. Null Checks

The Objects class has methods requireNonNull for convenient null checks of parameters. Here is the simplest one:

public void process(String directions) {
this.directions = Objects.requireNonNull(directions);
...
}

If directions is null, a NullPointerException is thrown, which doesn’t seem like a huge improvement at first. But consider working back from a stack trace. When you see a call to requireNonNull as the culprit, you know right away what you did wrong.

You can also supply a message string for the exception:

this.directions = Objects.requireNonNull(directions,
"directions must not be null");

9.5.4. ProcessBuilder

Prior to Java 5, the Runtime.exec method was the only way to execute an external command from within a Java application. Java 5 added the ProcessBuilder class that gives more control over the generated operating system process. In particular, with the ProcessBuilder, you can change the working directory.

Java 7 adds convenience methods to hook the standard input, output, and error streams of the process to files. For example,

ProcessBuilder builder = new ProcessBuilder(
"grep", "-o", "[A-Za-z_][A-Za-z_0-9]*");
builder.redirectInput(Paths.get("Hello.java").toFile());
builder.redirectOutput(Paths.get("identifiers.txt").toFile());
Process process = builder.start();
process.waitFor();


Image NOTE

Since Java 8, the Process class has a waitFor method with timeout:

boolean completed = process.waitFor(1, TimeUnit.MINUTES);


Also new in Java 7 is the inheritIO method of ProcessBuilder. It sets the standard input, output, and error streams of the process to those of the Java program. For example, when you run

ProcessBuilder builder = new ProcessBuilder("ls", "-al");
builder.inheritIO();
builder.start().waitFor();

then the output of the ls command is sent to System.out.

9.5.5. URLClassLoader

Suppose you want to write a Java program that automates execution of JUnit tests. To load the JUnitCore class, you need a class loader that reads the JUnit JAR files:

URL[] urls = {
new URL("file:junit-4.11.jar"),
new URL("file:hamcrest-core-1.3.jar")
};
URLClassLoader loader = new URLClassLoader(urls);
Class<?> klass = loader.loadClass("org.junit.runner.JUnitCore");

Before Java 7, code such as this could lead to resource leaks. Java 7 simply adds a close method to close the classloader. URLClassLoader now implements AutoCloseable, so you can use a try-with-resources statement:

try (URLClassLoader loader = new URLClassLoader(urls)) {
Class<?> klass = loader.loadClass("org.junit.runner.JUnitCore");
...
}


Image CAUTION

Don’t use any classes after the classloader has been closed. If you do, and those classes need to load other classes to do their work, they will fail.


9.5.6. BitSet

A BitSet is a set of integers that is implemented as a sequence of bits. The ith bit is set if the set contains the integer i. That makes for very efficient set operations. Union/intersection/complement are simple bitwise or/and/not.

Java 7 adds methods to construct bitsets.

byte[] bytes = { (byte) 0b10101100, (byte) 0b00101000 };
BitSet primes = BitSet.valueOf(bytes);
// {2, 3, 5, 7, 11, 13}
long[] longs = { 0x100010116L, 0x1L, 0x1L, 0L, 0x1L };
BitSet powersOfTwo = BitSet.valueOf(longs);
// {1, 2, 4, 8, 16, 32, 64, 128, 256}

The inverse methods are toByteArray and toLongArray.

byte[] bytes = powersOfTwo.toByteArray();
// [0b00010110, 1, 1, 0, 1, 0, 0, 0, 1, ...]


Image NOTE

As of Java 8, BitSet has a method stream that yields an IntStream.


Exercises

1. Implement a code segment that constructs a Scanner and a PrintWriter at the end of Section 9.1.1, “The try-with-resources Statement,” on page 180, without the try-with-resources statement. Be sure to close both objects, provided they have been properly constructed. You need to consider the following conditions:

• The Scanner constructor throws an exception.

• The PrintWriter constructor throws an exception.

• hasNext, next, or println throws an exception.

• in.close() throws an exception.

• out.close() throws an exception.

2. Improve on the preceding exercise by adding any exceptions thrown by in.close() or out.close() as suppressed exceptions to the original exception, if there was one.

3. When you rethrow an exception that you caught in a multi-catch clause, how do you declare its type in the throws declaration of the ambient method? For example, consider

public void process() throws ... {
try {
...
catch (FileNotFoundException | UnknownHostException ex) {
logger.log(Level.SEVERE, "...", ex);
throw ex;
}
}

4. In which other parts of the Java library did you encounter situations that would benefit from multi-catch or, even better, common exception superclasses? (Hint: XML parsing.)

5. Write a program that reads all characters of a file and writes them out in reverse order. Use Files.readAllBytes and Files.write.

6. Write a program that reads all lines of a file and writes them out in reverse order. Use Files.readAllLines and Files.write.

7. Write a program that reads the contents of a web page and saves it to a file. Use URL.openStream and Files.copy.

8. Implement the compareTo method of the Point class in Section 9.3.3, “Comparing Numeric Types,” on page 189, without using Integer.compareTo.

9. Given a class

public class LabeledPoint {
private String label;
private int x;
private int y;
...
}

implement the equals and hashCode methods.

10. Implement a compareTo method for the LabeledPoint class of the preceding exercise.

11. Using the ProcessBuilder class, write a program that calls grep -r to look for credit card numbers in all files in any subdirectory of the user’s home directory. Collect the numbers that you found in a file.

12. Turn the application of the preceding exercise into an applet or a Java Web Start implementation. Suppose you want to offer it to users as a security scan. Package it so that it will run on your JRE. What did you have to do? What would your users have to do to run it from your web site?