Saturday, June 23, 2012

Java 7 Changes

Java 7 was released about a year ago and is the latest version of the Java language. It includes some useful tweaks to the language as well as some improvements to the API. Some of the changes are described in detail below.

Warning: Long blog post ahead! >.<

1. Language Enhancements

Diamond Operator

When using generics in previous versions of Java, you always had to define the generic types twice--once in the variable definition and once in the class constructor call.

Map<Integer, String> map = new HashMap<Integer, String>();

In Java 7, the generic types in the constructor call no longer have to be repeated:

Map<Integer, String> map = new HashMap<>();

Strings in Switch Statements

Before, only integers and characters were allowed to be used in switch statements. In Java 7, strings can be used as well.

switch ("two"){
  case "one":
    ...
    break;
  case "two":
    ...
    break;
  case "three";
    ...
    break;
  default:
    ...
}

Note that the String.equals() method is used behind the scenes to do the string comparison, which means that the comparison is case-sensitive.

The try-with-resource statement

All Java programmers know how important it is to close resources when they are done being used (such as input and output streams). This is best done using a try-catch-finally block.

InputStream in = null;
OutputStream out = null;
try{
  in = ...;
  out = ...;
  ...
} catch (IOException e){
  ...
} finally{
  if (in != null){
      try{
        in.close();
      } catch (IOException e){}
  }
  if (out != null){
      try{
        out.close();
      } catch (IOException e){}
  }
}

Java 7 introduces a special try block, called try-with-resources, that automatically closes the resources for you. This reduces boiler-plate code, making your code shorter, easier to read, and easier to maintain. It also helps to eliminate bugs, since you no longer have to remember to close the resource.

try (InputStream in = ...; OutputStream out = ...){
  ...
} catch (IOException e){
  ...
}

In order to support this, the class must implement the AutoCloseable interface. However, classes that implement Closeable can also be auto-closed. This is because the Closeable interface was modified to extend AutoCloseable.

Multi-exception catch blocks

Previously, only one exception could be caught per catch block. This would sometimes lead to duplicated code:

try {
  ...
} catch (SQLException e) {
  System.out.println(e.getMessage());
  throw e;
} catch (IOException e) {
  System.out.println(e.getMessage());
  throw e;
}

Java 7 lets you put multiple exceptions in a single catch block, thus reducing code duplication:

try {
  ...
} catch (SQLException | IOException e) {
  System.out.println(e.getMessage());
  throw e;
}

Underscores in numeric literals

Sometimes, you have to hard-code a number in your Java code. If the number is long, it can be hard to read.

int i = 1000000;

How long does it take you to read this number? All those digits makes my eyes hurt. Is it ten million? One million? One hundred thousand? In Java 7, underscores can be added to the number to make it more readable.

int i = 1_000_000;

Binary literals

In Java 7, you can hard-code binary values. A binary value starts with "0b" (or "0B") and is followed by a sequence of "0"s and "1"s.

int one = 0b001;
int two = 0b010;
int six = 0b110;

2. New File System API - NIO 2.0

Java 7 adds a revamped file system API called NIO 2.0. Basically, you have a Path class that represents a path on the filesystem and a Files class that allows you to perform operations on an instance of Path, like deleting or copying a file.

Java IO and NIO 2.0 comparison

To give you a feel for the changes, here are two code samples that compare the APIs of the original IO framework and the new NIO 2.0 framework.

Java IO

import java.io.*;

public class JavaIO{
  public static void main(String args[]) throws Exception {
    //define a file
    File file = new File("/home/michael/my-file.txt");

    //create readers/writers
    BufferedReader bufReader = new BufferedReader(new FileReader(file));
    BufferedWriter bufWriter = new BufferedWriter(new FileWriter(file));

    //read file data into memory
    InputStream in = null;
    byte data[];
    try{
      in = new FileInputStream(file);
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      byte buffer[] = new byte[4096];
      int read;
      while ((read = in.read(buffer)) != -1){
        out.write(buffer, 0, read);
      }
      data = out.toByteArray();
    } finally{
      if (in != null){
        in.close();
      }
    }

    //write a string to a file
    PrintWriter writer = null;
    try{
      writer = new PrintWriter(file);
      writer.print("the data");
    } finally{
      if (writer != null){
        writer.close();
      }
    }

    //copy a file
    File target = new File("/home/michael/copy.txt");
    InputStream in2 = null;
    OutputStream out2 = null;
    try{
      in2 = new FileInputStream(file);
      out2 = new FileOutputStream(target);
      byte buffer[] = new byte[4096];
      int read;
      while ( (read = in2.read(buffer)) != -1){
        out2.write(buffer, 0, read);
      }
    } finally {
      if (in2 != null){
        in2.close();
      }
      if (out2 != null){
        out2.close();
      }
    }

    //delete a file
    boolean success = file.delete();
    if (!success){
      //the file couldn't be deleted...we don't know why!!
    }

    //create a directory
    File newDir = new File("/home/michael/mydir");
    success = newDir.mkdir();
    if (!success){
      //directory couldn't be created...we don't know why!!
    }
  }
}

NIO 2.0

import java.io.*;
import java.nio.file.*;
import java.nio.charset.*;

public class JavaNIO{
  public static void main(String args[]) throws Exception {
    //define a file
    Path file = Paths.get("/home/michael/my-file.txt");

    //create readers/writers
    BufferedReader reader = Files.newBufferedReader(file, Charset.defaultCharset());
    BufferedWriter writer = Files.newBufferedWriter(file, Charset.defaultCharset());

    //read file data into memory
    byte data[] = Files.readAllBytes(file);

    //write a string to a file
    Files.write(file, "the data".getBytes());

    //copy a file
    Path target = Paths.get("/home/michael/copy.txt");
    Files.copy(file, target);

    //delete a file
    //throws various exceptions depending on what went wrong
    Files.delete(file);

    //create a directory
    //throws various exceptions depending on what went wrong
    Path newDir = Paths.get("/home/michael/mydir");
    Files.createDirectory(newDir);
  }
}

As you can see, NIO 2.0 adds many convenience methods that remove the need to write a lot of boilerplate code. It also has more fine-grained error handling (for example, when deleting a file and creating a directory).

Directory monitoring

Also added to NIO 2.0 is the ability to monitor directories for changes. This allows your application to immediately respond to events such as files being deleted, modified, or renamed.

WatchService watchService = FileSystems.getDefault().newWatchService();
Path dir = Paths.get("/home/michael");
dir.register(watchService,
  StandardWatchEventKinds.ENTRY_CREATE,
  StandardWatchEventKinds.ENTRY_DELETE,
  StandardWatchEventKinds.ENTRY_MODIFY
);

while(true){
  WatchKey key = watchService.take();
  for (WatchEvent<?> event : key.pollEvents()){
    ...
  }
  key.reset();
}

Because the WatchSevice.take() method blocks while waiting for the next event, you should consider running this code in a separate thread.

Creating new File Systems (ZIP files)

Another feature in NIO 2.0 is the ability to create new file systems. One purpose for this interacting with ZIP files. NIO 2.0 basically treats a ZIP file as if it were a flash drive or another hard drive on your computer. You read, write, copy, and delete files on the ZIP file system as if they were ordinary files on a hard drive. The example below shows how create a ZIP file and add a file to it.

Map<String, String> env = new HashMap<>(); 
env.put("create", "true");
URI uri = URI.create("jar:file:/home/michael/my-zip.zip");

try (FileSystem zip = FileSystems.newFileSystem(uri, env)) {
  Path externalFilePath = Paths.get("my-file.txt");
  Path zipFilePath = zip.getPath("/my-file.txt");          
  Files.copy(externalFilePath, zipFilePath);
}

3. Fork/Join Framework

New fork/join classes were added to Java's Concurrency framework. They make it easier to divide a single task into many smaller tasks so the task can be completed using multi-threading. It uses a worker-stealing technique in which a worker thread will "steal" work from another worker thread if it finishes its work before the other. This helps to reduce the total amount of time it takes for a task to complete. Here's an example of how you would use fork/join to calculate the sum of an array of integers.

import java.util.concurrent.*;

public class ForkJoinDemo{
  public static void main(String args[]){
    ForkJoinPool pool = new ForkJoinPool();
    SumTask task = new SumTask(new int[]{0,1,2,3,4,5});
    Integer result = pool.invoke(task);
    System.out.println(result);
  }

  private static class SumTask extends RecursiveTask<Integer> {
    private final int[] numbers;
    private final int start, end;

    public SumTask(int[] numbers){
      this(numbers, 0, numbers.length - 1);
    }

    public SumTask(int[] numbers, int start, int end){
      this.numbers = numbers;
      this.start = start;
      this.end = end;
    }

    @Override
    public Integer compute(){
      if (start == end){
        return numbers[start];
      }
      if (end-start == 1){
        return numbers[start] + numbers[end];
      }
      int mid = (end+start)/2;
      SumTask t1 = new SumTask(numbers, start, mid);
      t1.fork();
      SumTask t2 = new SumTask(numbers, mid+1, end);
      return t2.compute() + t1.join();
    }
  } 
}

First, a ForkJoinPool object is created. Then, a task (SumTask) is instantiated and passed to the ForkJoinPool.invoke() method, which blocks until the task is complete. By default, ForkJoinPool will create one thread for each core in your computer.

4. Misc

Throwable.getSuppressed()

Java 7 adds a method called getSuppressed() to the Throwable class. This method allows the programmer to get any exceptions that were suppressed by the thrown exception.

An exception can be suppressed in a try-finally block or in the new try-with-resources block. In a try-finally block, if an exception is thrown from both the try and finally blocks, the exception thrown from the try block will be suppressed and the exception thrown from the finally block will be returned.

BufferedReader reader = null;
try{
  reader = ...;
  reader.readLine(); //IOException thrown (**suppressed**)
} finally {
  reader.close(); //IOException thrown
}

In Java 7's new try-with-resources block, it is the opposite--the exception thrown when it closes resource is suppressed, while the exception thrown from the try block is returned:

try (BufferedReader reader = ...){
  reader.readLine(); //IOException thrown
}
//IOException thrown when "reader" is closed (**suppressed**)

In previous Java versions, there was no way to get this suppressed exception. But in Java 7, calling the new Throwable.getSuppressed() method will return an array of all suppressed exceptions.

Shaped and Translucent Windows in Swing

You can now create non-square, transparent windows in Swing. Examples can be found in the Java Swing tutorial.


These are just some of the changes that were added to Java 7. Out of all the changes I've mentioned, my favorite is the file monitoring functionality in the NIO 2.0 framework. This is completely new functionality that didn't exist before in previous Java versions. What's your favorite?? Leave a note in the comments below.

The complete Java 7 release notes can be found on Oracle's website.

3 comments:

Anonymous said...

Awesome!

Anonymous said...

Awesome!

# Rav said...

Very good briefing..