Chapter 12

Using Collections and Streams (When Arrays Aren't Good Enough)

In This Chapter

arrow Facing the limitations of arrays

arrow Dealing with a bunch of objects at once

arrow Using Java's cool functional programming features

arrow Developing code for multicore processors

Chapter 11 is about arrays. With an array, you can manage a bunch of things all at once. In a hotel-management program, you can keep track of all the rooms. You can quickly find the number of people in a room or find one of the vacant rooms.

However, arrays don’t always fit the bill. In this chapter, you find out where arrays fall short and how collections can save the day.

Understanding the Limitations of Arrays

Arrays are nice, but they have some serious limitations. Imagine that you store customer names in some predetermined order. Your code contains an array, and the array has space for 100 names.

  String name[] = new String[100];
for (int i = 0; i < 100; i++) {
   name[i] = new String();
}

All is well until, one day, customer number 101 shows up. As your program runs, you enter data for customer 101, hoping desperately that the array with 100 components can expand to fit your growing needs.

No such luck. Arrays don’t expand. Your program crashes with an Array
IndexOutOfBoundsException.

“In my next life, I’ll create arrays of length 1,000,” you say to yourself. And when your next life rolls around, you do just that.

  String name[] = new String[1000];
for (int i = 0; i < 1000; i++) {
   name[i] = new String();
}

But during your next life, an economic recession occurs. Instead of having 101 customers, you have only 3 customers. Now you’re wasting space for 1,000 names when space for 3 names would do.

And what if no economic recession occurs? You’re sailing along with your array of size 1,000, using a tidy 825 spaces in the array. The components with indices 0 through 824 are being used, and the components with indices 825 through 999 are waiting quietly to be filled.

One day, a brand-new customer shows up. Because your customers are stored in order (alphabetically by last name, numerically by Social Security number, whatever), you want to squeeze this customer into the correct component of your array. The trouble is that this customer belongs very early on in the array, at the component with index 7. What happens then?

You take the name in component number 824 and move it to component 825. Then you take the name in component 823 and move it to component 824. Take the name in component 822 and move it to component 823. You keep doing this until you’ve moved the name in component 7. Then you put the new customer’s name into component 7. What a pain! Sure, the computer doesn’t complain. (If the computer has feelings, it probably likes this kind of busy work.) But as you move around all these names, you waste processing time, you waste power, and you waste all kinds of resources.

“In my next life, I’ll leave three empty components between every two names.” And of course, your business expands. Eventually you find that three aren’t enough.

Collection Classes to the Rescue

The issues in the previous section aren’t new. Computer scientists have been working on these issues for a long time. They haven’t discovered any magic one-size-fits-all solution, but they’ve discovered some clever tricks.

The Java API has a bunch of classes known as collection classes. Each collection class has methods for storing bunches of values, and each collection class’s methods use some clever tricks. For you, the bottom line is as follows: Certain collection classes deal as efficiently as possible with the issues raised in the previous section. If you have to deal with such issues when writing code, you can use these collection classes and call the classes’ methods. Instead of fretting about a customer whose name belongs in position 7, you can just call a class’s add method. The method inserts the name at a position of your choice and deals reasonably with whatever ripple effects have to take place. In the best circumstances, the insertion is very efficient. In the worst circumstances, you can rest assured that the code does everything the best way it can.

Using an ArrayList

One of the most versatile of Java’s collection classes is the ArrayList. Listing 12-1 shows you how it works.

Listing 12-1: Working with a Java Collection

  import static java.lang.System.out;
import java.util.Scanner;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

public class ShowNames {

   public static void main(String args[])
                       throws IOException {
      ArrayList<String> people =
                    new ArrayList<String>();
      Scanner diskScanner =
         new Scanner(new File("names.txt"));

      while (diskScanner.hasNext()) {
         people.add(diskScanner.nextLine());
      }

      people.remove(0);
      people.add(2, "Jim Newton");
    
      for (String name : people) {
         out.println(name);
      }

      diskScanner.close();
   }
}

Figure 12-1 shows you a sample names.txt file. The code in Listing 12-1 reads that names.txt file and prints the stuff in Figure 12-2.

9781118407806-fg1201.tif

Figure 12-1: Several names in a file.

9781118407806-fg1202.tif

Figure 12-2: The code in Listing 12-1 changes some of the names.

All the interesting things happen when you execute the remove and add methods. The variable named people refers to an ArrayList object. When you call that object’s remove method,

  people.remove(0);

you eliminate a value from the list. In this case, you eliminate whatever value is in the list’s initial position (the position numbered 0). So in Listing 12-1, the call to remove takes the name Barry Burd out of the list.

That leaves only eight names in the list, but then the next statement,

  people.add(2, "Jim Newton");

inserts a name into position number 2. (After Barry is removed, position number 2 is the position occupied by Harry Spoonswagler, so Harry moves to position 3, and Jim Newton becomes the number 2 man.)

Notice that an ArrayList object has two different add methods. The method that adds Jim Newton has two parameters: a position number and a value to be added. Another add method

  people.add(diskScanner.nextLine());

takes only one parameter. This statement takes whatever name it finds on a line of the input file and appends that name to the end of the list. (The add method with only one parameter always appends its value to what’s currently the end of the ArrayList object.)

The last few lines of Listing 12-1 contain an enhanced for loop. Like the loop in Listing 11-3, the enhanced loop in Listing 12-1 has the following form:

  for (variable-type variable-name : range-of-values)

In Listing 12-1, the variable-type is String, the variable-name is name, and the range-of-values includes the things stored in the people collection. During an iteration of the loop, name refers to one of the String values stored in people. (So if the people collection contains nine values, then the for loop goes through nine iterations.) During each iteration, the statement inside the loop displays a name on the screen.

Using generics

Look again at Listing 12-1, shown earlier, and notice the funky ArrayList declaration:

  ArrayList<String> people = new ArrayList<String>();

Starting with Java 5.0, each collection class is generified. That ugly-sounding word means that every collection declaration should contain some angle-bracketed stuff, such as <String>. The thing that’s sandwiched between < and > tells Java what kinds of values the new collection may contain. For example, in Listing 12-1, the words ArrayList<String> people tell Java that people is a bunch of strings. That is, the people list contains String objects (not Room objects, not Account objects, not Employee objects, nothing other than String objects).

remember.eps Most of this section's material applies only to Java 5.0 and later. You can't use generics in any version of Java before Java 5.0. For more about generics, see the sidebar. And for more about Java's version numbers, see Chapter 2.

In Listing 12-1 the words ArrayList<String> people say that the people variable can refer only to a collection of String values. So from that point on, any reference to an item from the people collection is treated exclusively as a String. If you write

  people.add(new Room());

then the compiler coughs up your code and spits it out because a Room (created in Chapter 11) isn’t the same as a String. (This coughing and spitting happens even if the compiler has access to the Room class’s code — the code in Chapter 11.) But the statement

  people.add("George Gow");

is just fine. Because "George Gow" has type String, the compiler smiles happily.

technicalstuff.eps Java 7 and later versions have a cool feature allowing you to abbreviate generic declarations. In Listing 12-1, you can write ArrayList <String> people = new ArrayList<>() without repeating the word String a second time in the declaration. The <> symbol without any words inside it is called a diamond operator. The diamond operator saves you from having to rewrite stuff like <String> over and over.

Testing for the presence of more data

Here’s a pleasant surprise. When you write a program like the one shown previously in Listing 12-1, you don’t have to know how many names are in the input file. Having to know the number of names may defeat the purpose of using the easily expandable ArrayList class. Instead of looping until you read exactly nine names, you can loop until you run out of data.

The Scanner class has several nice methods such as hasNextInt, hasNext
Double, and plain old hasNext. Each of these methods checks for more input data. If there’s more data, the method returns true. Otherwise, the method returns false.

Listing 12-1 uses the general-purpose hasNext method. This hasNext method returns true as long as there’s anything more to read from the program’s input. So after the program scoops up that last Hugh R. DaReader line in Figure 12-1, the subsequent hasNext call returns false. This false condition ends execution of the while loop and plummets the computer toward the remainder of the Listing 12-1 code.

The hasNext method is very handy. In fact, hasNext is so handy that it's part of a bigger concept known as an iterator, and iterators are baked into all of Java's collection classes.

Using an iterator

An iterator spits out a collection's values, one after another. To obtain a value from the collection, you call the iterator's next method. To find out whether the collection has any more values in it, you call the iterator's hasNext method. Listing 12-2 uses an iterator to display people's names.

Listing 12-2: Iterating through a Collection

  import static java.lang.System.out;

import java.util.Iterator;
import java.util.Scanner;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

public class ShowNames {

   public static void main(String args[])
                       throws IOException {
      ArrayList<String> people =
                    new ArrayList<String>();
      Scanner diskScanner =
         new Scanner(new File("names.txt"));

      while (diskScanner.hasNext()) {
         people.add(diskScanner.nextLine());
      }

      people.remove(0);
      people.add(2, "Jim Newton");
    
      Iterator<String> iterator = people.iterator();
      while (iterator.hasNext()) {
         out.println(iterator.next());
      }
      
      diskScanner.close();
   }
}

You can replace the enhanced for loop at the end of Listing 12-1 with the boldface code in Listing 12-2. When you do, you get the get the same output as before. (You get the output in Figure 12-2.) In Listing 12-2, the first boldface line of code creates an iterator from the people collection. The second and third lines call the iterator's hasNext and next methods to grab all the objects stored in the people collection — one for each iteration of the loop. These lines display each of the people collection's values.

Which is better? An enhanced for loop or an iterator? Java programmers prefer the enhanced for loop because the for loop involves less baggage — no iterator object to carry from one line of code to the next. But as you see later in this chapter, the most programming-enhanced feature can be upgraded, streamlined, tweaked, and otherwise reconstituted. There's no end to the way you can improve upon your code.

Java's many collection classes

The ArrayList class that I use in many of this chapter's examples is only the tip of the Java collections iceberg. The Java library contains many collections classes, each with its own advantages. Table 12-1 contains an abbreviated list.

Table 12-1 Some Collection Classes

Class Name

Characteristic

ArrayList

A resizable array.

LinkedList

A list of values, each having a field that points to the next one in the list.

Stack

A structure that grows from bottom to top. The structure is optimized for access to the topmost value. You can easily add a value to the top or remove the value from the top.

Queue

A structure that grows at one end. The structure is optimized for adding values to one end (the rear) and removing values from the other end (the front).

PriorityQueue

A structure, like a queue, that lets certain (higher-priority) values move toward the front.

HashSet

A collection containing no duplicate values.

HashMap

A collection of key/value pairs.

Each collection class has its own set of methods (in addition to the methods that it inherits from AbstractCollection, the ancestor of all collection classes).

ontheweb_fmt.eps To find out which collection classes best meet your needs, visit the Java API documentation pages at http://docs.oracle.com/javase/8/docs/api.

New in Java 8: Functional Programming

From 1953 to 1957, John Backus and others developed the FORTRAN programming language, which contained the basic framework for thousands of 20th century programming languages. The framework has come to be known as imperative programming because of its “do this, then do that” nature.

A few years after the rise of FORTRAN, John McCarthy created another language named Lisp. Unlike FORTRAN, the underlying framework for Lisp is functional programming. In a purely functional program, you avoid writing “do this, then do that.” Instead, you write things like “here's how you'll be transforming this into that when you get around to doing the transformation.”

For one reason or another, imperative programming became the dominant mode. As a result, Java is fundamentally an imperative programming language. But recently, functional programming has emerged as a powerful and useful way of thinking about code.

I've read lots of articles and attended dozens of presentations describing functional programming. Frankly, most of these descriptions aren't very helpful. Some of them feed you a bunch of rigorous definitions and expect you to form your own intuitions. Others describe the intuitive ideas without giving you any concrete examples to work with.

So instead of describing functional programming in detail, I start this section with an analogy. Then, in the rest of this chapter, I present some Java 8 examples.

The analogy that I use to describe functional programming is very rough. A friend of mine called this analogy a stretch because it applies to many different programming frameworks, not only to functional programming. One way or another, I think the analogy is helpful.

Here's the analogy: Imagine a programming problem as a cube, and imagine an imperative programming solution as a way of slicing up the cube into manageable pieces. (See Figure 12-3.)

9781118407806-fg1203.tif

Figure 12-3: Imperative programming slices up a problem.

All was well until 2007 when, for the first time, computers sold to consumers had multicore processors. A multicore processor can perform more than one instruction at a time. Figure 12-4 shows what happens when you try to squeeze an imperative program into a multicore processor.

9781118407806-fg1204.tif

Figure 12-4: An imperative program's pieces don't fit neatly into a multicore chip.

To get the most out of a four-core processor, you divide your code into four pieces — one piece for each core. But with imperative programming, your program's pieces don't fit neatly into your processor's cores.

technicalstuff.eps In imperative programming, your code's pieces interact with one another. All the pieces might be updating the current price of Oracle stock shares (ticker symbol ORCL). The simultaneous updates become tangled. It's like several high-school boys asking the same girl to the senior prom. Nothing good ever comes of it. You've experienced the same phenomenon if you've ever clicked a website's Purchase button only to learn that the item you're trying to purchase is out of stock. Someone else completed a purchase while you were filling in your credit card information. Too many customers were grabbing for the same goods at the same time.

Figure 12-3 suggests that, with imperative programming, you divide your code into several pieces. Functional programming also divides code into pieces, but it does so along different lines. (See Figure 12-5.) And here's the good news: With functional programming, the pieces of the code fit neatly into the processor's cores. (See Figure 12-6.)

9781118407806-fg1205.tif

Figure 12-5: Functional programming slices the problem along different lines.

9781118407806-fg1206.tif

Figure 12-6: A functional program's pieces fit neatly into a multicore chip.

Solving a problem the old-fashioned way

In Chapter 11, you used arrays to manage the Java Motel. But that venture is behind you now. You've given up the hotel business. (You tell people that you decided to move on. But in all honesty, the hotel was losing a lot of money. According to the United States bankruptcy court, the old Java Motel is currently in Chapter 11.)

Since leaving the hotel business, you’ve transitioned into online sales. Nowadays, you run a website that sells books, DVDs, and other content-related items. (Barry Burd's Java For Dummies, 6th Edition is currently your best seller, but that's beside the point.)

In your world, the sale of a single item looks something like the stuff in Listing 12-3. Each sale has an item and a price.

Listing 12-3: The Sale Class

  public class Sale {
   String item;
   double price;

   public Sale(String item, double price) {
      this.item = item;
      this.price = price;
   }
}

To make use of the Sale class, you create a small program. The program totals up the sales on DVDs. The program is shown in Listing 12-4.

Listing 12-4: Using the Sale Class

  import java.text.NumberFormat;
import java.util.ArrayList;

public class TallySales {

   public static void main(String[] args) {
      ArrayList<Sale> sales = new ArrayList<Sale>();
      NumberFormat currency =
         NumberFormat.getCurrencyInstance();

      fillTheList(sales);

      double total = 0;
      for (Sale sale : sales) {
         if (sale.item.equals("DVD")) {
            total += sale.price;
         }
      }

      System.out.println(currency.format(total));
   }

   static void fillTheList(ArrayList<Sale> sales) {
      sales.add(new Sale("DVD", 15.00));
      sales.add(new Sale("Book", 12.00));
      sales.add(new Sale("DVD", 21.00));
      sales.add(new Sale("CD", 5.25));
   }
}

In Chapter 11, you step through an array by using an enhanced for statement. Listing 12-4 has its own enhanced for statement. But in Listing 12-4, the enhanced for statement steps through the values in a collection. Each such value is a sale. The loop repeatedly checks a sale to find out whether the item sold is a DVD. If so, the code adds the sale's price to the running total. The program's output is $36.00— the running total displayed as a currency amount.

The scenario in Listing 12-4 isn't unusual. You have a collection of things (such as sales). You step through the things in the collection, finding the things that meet certain criteria (being the sale of a DVD, for example). You grab a certain value (such as the sale price) of each thing that meets your criteria. Then you do something useful with the values that you've grabbed (for example, adding the values together).

Here are some other examples:

  • Step through your list of employees. Find each employee whose performance evaluation has score 3 or above. Give each such employee a $100 bonus and then determine the total amount of money you'll pay in bonuses.
  • Step through your list of customers. For each customer who has shown interest in buying a smartphone, send the customer an e-mail about this month's discount plans.
  • Step through the list of planets that have been discovered. For each M-class planet, find the probability of finding intelligent life on that planet. Then find the average of all such probabilities.

This scenario is so common that it's worth finding better and better ways to deal with the scenario. One way to deal with it is to use some of the new functional programming features in Java 8.

Streams

The “Using an iterator” section introduces iterators. You use an iterator's next method to spit out a collection's values. Java 8 takes this concept one step further with the notion of a stream. A stream is like an iterator except that, with a stream, you don't have to call a next method. After being created, a stream spits out a collection's values automatically. To get values from a stream, you don't call a stream's next method. In fact, a typical stream has no next method.

How does this work as part of a Java program? How do you create a stream that spits out values? How does the stream know when to start spitting, and where does the stream aim when it spits? For answers to these and other questions, read the next several sections.

Lambda expressions

In the 1930s, mathematician Alonzo Church used the Greek letter lambda (λ) to represent a certain mathematical construct that's created on-the-fly.* Over the next several decades, the idea survived quietly in mathematics and computer science journals. These days, in Java 8, the term lambda expression represents a short piece of code that serves as both a method declaration and a method call, all created on-the-fly.

A dirt-simple lambda expression

Here's an example of a lambda expression:

  (sale) -> sale.price

Figure 12-7 describes the lambda expression's meaning.

9781118407806-fg1207.tif

Figure 12-7: From a particular sale, get the price.

A lambda expression is a concise way of defining a method and calling the method without even giving the method a name. The lambda expression in Figure 12-7 does (roughly) what the following code does:

  double getPrice(Sale sale) {
   return sale.price;
}

getPrice(sale);

(Remember, the Sale class in Listing 12-3 has no getPrice method of its own.) The lambda expression in Figure 12-7 takes objects from a stream and calls a method resembling getPrice on each object. The result is a new stream — a stream of double values.

A more interesting example

Here's another lambda expression:

  (sale) -> sale.item.equals("DVD")

Figure 12-8 describes the lambda expression's meaning.

9781118407806-fg1208.tif

Figure 12-8: Does the item that's being sold happen to be a DVD?

The lambda expression in Figure 12-8 does (roughly) what the following code does:

  boolean itemIsDVD(Sale sale) {
   if sale.item.equals("DVD") {
      return true;
   } else {
      return false;
   }
}

itemIsDVD(sale);

The lambda expression in Figure 12-8 takes objects from a stream and calls a method resembling itemIsDVD on each object. The result is a bunch of boolean values — true for each sale of a DVD and false for a sale of something other than a DVD.

tip.eps With or without lambda expressions, you can rewrite the itemIsDVD method with a one-line body:

  boolean itemIsDVD(Sale sale) {
   return sale.item.equals("DVD");
}

A lambda expression with two parameters

Consider the following lambda expression:

  (price1, price2) -> price1 + price2

Figure 12-9 describes the new lambda expression's meaning.

9781118407806-fg1209.tif

Figure 12-9: Add two prices.

The lambda expression in Figure 12-9 does (roughly) what the following code does:

  double sum(double price1, double price2) {
   return price1 + price2;
}

sum(price1, price2);

The lambda expression in Figure 12-9 takes values from a stream and calls a method resembling sum to combine the values. The result is the total of all prices.

The black sheep of lambda expressions

Here’s an interesting lambda expression:

  (sale) -> System.out.println(sale.price)

This lambda expression does (roughly) what the following code does:

  void display(Sale sale) {
   System.out.println(sale.price);
}

display(sale);

The lambda expression takes objects from a stream and calls a method resembling display on each object. In the display method's header, the word void indicates that the method doesn't return a value. When you call the display method (or you use the equivalent lambda expression), you don't expect to get back a value. Instead, you expect the code to do something in response to the call (something like displaying text on the computer's screen).

To draw a sharp distinction between returning a value and “doing something,” functional programmers have a name for “doing something without returning a value.” They call that something a side effect. In functional programming, a side effect is considered a second-class citizen, a last resort, a tactic that you use when you can't simply return a result. Unfortunately, displaying information on a screen (something that so many computer programs do) is a side effect. Any program that displays output (on a screen, on paper, or as tea leaves in a cup) isn't a purely functional program.

A taxonomy of lambda expressions

Java 8 divides lambda expressions into about 45 different categories. Table 12-2 lists a few of the categories.

Table 12-2 A Few Kinds of Lambda Expressions

Name

Description

Example

Function

Accepts one parameter; produces a result of any type

(sale) -> sale.
 price

Predicate

Accepts one parameter; produces a boolean valued result

(sale) ->
 sale.item.
 equals("DVD")

BinaryOperator

Accepts two parameters of the same type; produces a result of the same type

(price1, price2)
 -> price1 + price2

Consumer

Accepts one parameter; produces no result

(sale) ->
 System.out.
 println(sale.
 price)

remember.eps The categories in Table 12-2 aren't mutually exclusive. For example, every Predicate is a Function. (Every Predicate accepts one parameter and returns a result. The result happens to be boolean.)

Using streams and lambda expressions

Java 8 has fancy methods that make optimal use of streams and lambda expressions. With streams and lambda expressions, you can create an assembly line that elegantly solves this chapter's sales problem. Unlike the code in Listing 12-4, the new assembly line solution uses concepts from functional programming.

The assembly line consists of several methods. Each method takes the data, transforms the data in some way or other, and hands its results to the next method in line. Figure 12-10 illustrates the assembly line for this chapter's sales problem.

9781118407806-fg1210.tif

Figure 12-10: A functional programming assembly line.

In Figure 12-10, each box represents a bunch of raw materials as they're transformed along an assembly line. Each arrow represents a method (or metaphorically, a worker on the assembly line).

For example, in the transition from the second box to the third box, a worker method (the filter method) sifts out sales of items that aren't DVDs. Imagine Lucy Ricardo standing between the second and third boxes, removing each book or CD from the assembly line and tossing it carelessly onto the floor.

The parameter to Java's filter method is a Predicate — a lambda expression whose result is boolean. (See Tables 12-2 and 12-3.) The filter method in Figure 12-10 sifts out items that don't pass the lambda expression's true / false test.

cross-reference.eps For some help understanding the words in the third column of Table 12-3 (Predicate, Function and BinaryOperator), see the preceding section, “A taxonomy of lambda expressions.”

1203

In Figure 12-10, in the transition from the third box to the fourth box, a worker method (the map method) pulls the price out of each sale. From that worker's place onward, the assembly line contains only price values.

To be more precise, Java's map method takes a Function such as

  (sale) -> sale.price

and applies the Function to each value in a stream. (See Tables 12-2 and 12-3.) So the map method in Figure 12-10 takes an incoming stream of sale objects and creates an outgoing stream of price values.

In Figure 12-10, in the transition from the fourth box to the fifth box, a worker method (the reduce method) adds up the prices of DVD sales. Java's reduce method takes two parameters:

  • The first parameter is an initial value.

    In Figure 12-10, the initial value is 0.0.

  • The second parameter is a BinaryOperator. (See Tables 12-2 and 12-3.)

    In Figure 12-10, the reduce method's BinaryOperator is

      (price1, price2) -> price1 + price2

The reduce method uses its BinaryOperator to combine the values from the incoming stream. The initial value serves as the starting point for all the combining. So, in Figure 12-10, the reduce method does two additions. (See Figure 12-11.)

9781118407806-fg1211.tif

Figure 12-11: The reduce method adds two values from an incoming stream.

For comparison, imagine calling the method

  reduce(10.0, (value1, value2) -> value1 * value2)

with the stream whose values include 3.0, 2.0, and 5.0. The resulting action is shown in Figure 12-12.

9781118407806-fg1212.tif

Figure 12-12: The reduce method multiplies values from an incoming stream.

technicalstuff.eps You might have heard of Google's MapReduce programming model. The similarity between the programming model's name and the Java method names map and reduce is not a coincidence.

Taken as a whole, the entire assembly line shown in Figure 12-10 adds up the prices of DVDs sold. Listing 12-5 contains a complete program using the streams and lambda expressions of Figure 12-10.

Listing 12-5: Living the Functional Way of Life

  import java.text.NumberFormat;
import java.util.ArrayList;

public class TallySales {

   public static void main(String[] args) {
      ArrayList<Sale> sales = new ArrayList<Sale>();
      NumberFormat currency =
         NumberFormat.getCurrencyInstance();

      fillTheList(sales);
      System.out.println(currency.format(
      sales.stream()
         .filter((sale) -> sale.item.equals("DVD"))
         .map((sale) -> sale.price)
         .reduce
          (0.0, (price1, price2) -> price1 + price2)));
   }

   static void fillTheList(ArrayList<Sale> sales) {
      sales.add(new Sale("DVD", 15.00));
      sales.add(new Sale("Book", 12.00));
      sales.add(new Sale("DVD", 21.00));
      sales.add(new Sale("CD", 5.25));
   }
}

The boldface code in Listing 12-5 is one big Java statement. The statement is a sequence of method calls. Each method call returns an object, and each such object is the thing before the dot in the next method call. That's how you form the assembly line.

For example, at the start of the boldface code, the name sales refers to an ArrayList object. Each ArrayList object has a stream method. So in Listing 12-5, sales.stream() is a call to that ArrayList object's stream method.

The stream method returns an instance of Java's Stream class. (What a surprise!) So sales.stream() refers to a Stream object. (See Figure 12-13.)

9781118407806-fg1213.tif

Figure 12-13: Getting all DVD sales.

Every Stream object has a filter method. So sales.stream().filter
​((sale) -> sale.item.equals("DVD")) is a call to the ​Stream object's filter method. (Refer to Figure 12-13.)

The pattern continues. The Stream object's map method returns yet another Stream object — a Stream object containing prices. (See Figure 12-14.) To that Stream of prices you apply the reduce method, which yields one double value — the total of the DVD prices. (See Figure 12-15.)

9781118407806-fg1214.tif

Figure 12-14: Getting the price from each DVD sale.

9781118407806-fg1215.tif

Figure 12-15: Getting the total price of all DVD sales.

Why bother?

The chain of method calls in Listing 12-5 accomplishes everything that the loop in Listing 12-4 accomplishes. But the code in Figure 12-15 uses concepts from functional programming. So what's the big deal? Are you better off with Listing 12-5 than with Listing 12-4?

You are. For the past several years, the big trend in chip design has been multicore processors. With several cores, a processor can execute several statements at once, speeding up a program's execution by a factor of 2, or 4 or 8, or even more. Programs run much faster if you divide the work among several cores. But how do you divide the work?

You can modify the imperative code in Listing 12-4. For example, with some fancy features, you can hand different loop iterations to different cores. But the resulting code is very messy. For the code to work properly, you have to micromanage the loop iterations, checking carefully to make sure that the final total is correct.

In contrast, the functional code is easy to modify. To take advantage of multicore processors, you change only one word in Listing 12-5!

  sales.parallelStream()
.filter((sale) -> sale.item.equals("DVD"))
.map((sale) -> sale.price)
.reduce
   (0.0, (price1, price2) -> price1 + price2)

A parallel stream is like an ordinary stream except that a parallel stream's objects aren't processed in any particular order. A parallel stream's values don't depend on one another, so you can process several of the values at the same time. If your processor has more than one core (and most processors do these days) you can divide the stream's values among the available cores. Each core works independently of the others, and each core dumps its result into a final reduce method. The reduce method combines the cores' results into a final tally, and your answer is computed in lightning-fast time. It's a win for everyone.

Method references

Take a critical look at the last lambda expression in Listing 12-5:

  (price1, price2) -> price1 + price2)

This expression does roughly the same work as a sum method. (In fact, you can find a sum method's declaration in the earlier “Lambda expressions” section.) If your choice is between typing a three-line sum method and typing a one-line lambda expression, you'll probably choose the lambda expression. But what if you have a third alternative? Instead of typing your own sum method, you can refer to an existing sum method. Using an existing method is the quickest and safest thing to do.

As luck would have it, Java 8 has a class named Double. The Double class contains a static sum method. You don't have to create your own sum method. If you run the following code:

  double i = 5.0, j = 7.0;
System.out.println(Double.sum(i, j));

the computer displays 12.0. So, instead of typing the price1 + price2 lambda expression in Listing 12-5, you can create a method reference — an expression that refers to an existing method.

  sales.stream()
   .filter((sale) -> sale.item.equals("DVD"))
   .map((sale) -> sale.price)
   .reduce(0.0, Double::sum)

The expression Double::sum refers to the sum method belonging to Java's Double class. When you use this Double::sum method reference, you do the same thing that the last lambda expression does in Listing 12-5. Everybody is happy.

For information about static methods, see Chapter 10.

cross-reference.eps Java's Double class is a lot like the Integer class that I describe in Chapter 11. The Double class has methods and other tools for dealing with double values.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.189.67