Java functional programming of eight: strings and method references

  • 2020-04-01 03:30:50
  • OfStack

Chapter 3 strings, comparators and filters

Some of the methods introduced by the JDK are useful for writing functional style code. Some of the classes and interfaces in the JDK library are already familiar to us, such as String, and in order to get away from the old style we were used to, we had to actively look for opportunities to use these new methods. Also, when we need to use an anonymous inner class with only one method, we can now replace it with a lambda expression instead of having to write as much as we did before.

In this chapter, we'll use lambda expressions and method references to traverse strings, implement the Comparator interface, view files in directories, and monitor changes to files and directories. Some of the methods described in the previous chapter will continue to appear here to help us better accomplish these tasks. The new techniques you learn can help make tedious code simple, fast to implement and easy to maintain.

Traversal string

The chars() method is a new method in the String class that is part of the CharSequence interface. It's a useful tool for quickly traversing a String's sequence of characters. With this inner iterator, we can easily manipulate the individual characters in the string. So let's try it out with a string. Here, in passing, are several ways to use method references.


final String str = "w00t";
str.chars()
     .forEach(ch -> System.out.println(ch));

The chars() method returns a Stream object that we can iterate over using its internal iterator, forEach(). In the iterator, we have direct access to the characters in the string. The following is the output of traversing the string and printing the individual characters.

119
48
48
116

This is not the result we want. We want to see letters and output Numbers. This is because the chars() method returns an integer Stream instead of a character. Let's take a look at the API and optimize the output.

In the previous code, we created a lambda expression as an input to the forEach method. It simply passes the argument to a println() method. Because this is so common, we can use the Java compiler to simplify the code. Replace it with a method reference, as we did in the method reference on page 25, and let the compiler do the routing for us.

We have seen how to create a method reference for an instance method. For example, the name.touppercase () method, the method reference is String::toUpperCase. In the following example, we call an instance method that statically references system.out. Method references to the left of the two colons can be a class name or an expression. With this flexibility, we can easily create a reference to the println() method, as shown below.


str.chars()
     .forEach(System.out::println);

As you can see, the Java compiler does a clever job of routing parameters. Recall that lambda expressions and method references can only appear in places that receive functional interfaces, where the Java compiler generates a corresponding method. The method we used earlier referred to String::toUpperCase, passed to the argument that generated the method, and ended up being the target object for the method call, like this: parameter.touppercase (). This is because the method reference is based on the class name (String). The method reference in the above example is based on an expression, which is an instance of PrintStream, referenced through system.out. Since the object for the method call already exists, the Java compiler decides to use the arguments in the generate method as arguments to this println method: system.out.println (name).

There are also two scenarios where a method reference is passed, one is the object to be traversed, and of course the target object of the method call, such as name.touppercase, and the other is the parameter of the method call, such as system.out.println (name).

The code is much cleaner with method references, but we'll have to dig into how it works. Once we're familiar with the method references, we can figure out things like parameter routing ourselves.

Although the code in this example is clean enough, the output is not good enough. What we want to see is a letter that turns out to be a number. To solve this problem, let's write a method that outputs an int as a letter.


private static void printChar(int aChar) {
      System.out.println((char)(aChar));
}

Method references can be used to easily optimize the output.

str.chars()
     .forEach(IterateString::printChar);

Now, chars() returns an int, but it doesn't matter, we'll convert it to a character when we need to print it. This time the output is letters.

w
0
0
t

If we want to start with a character instead of an int, we can directly convert an int to a character after calling chars:

str.chars()
     .mapToObj(ch -> Character.valueOf((char)ch))
     .forEach(System.out::println);

Here we are using an internal iterator of the Stream returned by chars(), but this is not the only method available. Once we get the Stream object, we're left to our own devices like map(),filter(),reduce(), etc. We can use the filter() method to filter out those characters that are Numbers:

str.chars()
     .filter(ch -> Character.isDigit(ch))
     .forEach(ch -> printChar(ch));

This way we can only see the Numbers when output:

0
0

Similarly, we can use method references in addition to passing the lambda expression to the filter() and forEach() methods.

str.chars()
     .filter(Character::isDigit)
     .forEach(IterateString::printChar);

The method reference here omits the routing of redundant parameters. In this example, we also see a different use of the reference from the previous two methods. The first time we refer to an instance method, and the second time we refer to a method on a static reference (system.out). This time it is a reference to a static method -- the method reference has been paying the price.

References to instance methods and static methods look the same: for example String::toUpperCase and Character::isDigit. The compiler determines whether the method is an instance method or a static method to determine how to route the parameters. If it is an instance method, it USES the generated method's input as the target object for the method call, such as parameter,toUpperCase(); There are exceptions, such as the target object for the method call already specified, such as System::out.println(). In addition, if the method is static, the input parameter of the generated method will be used as the parameter of the referenced method, such as character.isdigit (parameter). Appendix 2, page 152, has detailed usage and syntax for method references.

While method references are convenient to use, there is one problem -- ambiguity caused by method naming conflicts. If the matching method has both an instance method and a static method, the compiler will report an error because the method is ambiguous. So, for example, Double::toString, we really want to convert a Double to a String, but the compiler doesn't know whether to call the instance method of public String toString() or call the public static String toString(Double) method, because both methods are of the Double class. If that happens to you, don't get discouraged and just do it with a lambda expression.

Once we get used to functional programming, we can switch back and forth between lambda expressions and method references at will.

In this section we use a new method from Java 8 to iterate over strings. Let's take a look at some of the improvements to the Comparator interface.


Related articles: