Reading Notes of "java 8 Actual Warfare" - Chapter 8 Reconstruction, Testing and Debugging

Keywords: Java Lambda Database

1. Refactoring code to improve readability and flexibility

1. Improving code readability

New features of Java 8 can also help improve code readability:

  • With Java 8, you can reduce verbose code and make it easier to understand.
  • With method references and Stream API s, your code becomes more intuitive

Here we will introduce three simple refactoring methods, Lambda expressions, method references, and Stream to improve the readability of program code:

  • Refactoring code to replace anonymous classes with Lambda expressions
  • Reconstructing Lambda Expressions by Method Reference
  • Reconstructing imperative data processing with Stream API

2. Conversion from Anonymous Classes to Lambda Expressions

  • In anonymous classes, this represents the class itself, but in Lambda, it represents the containing class. Second, anonymous classes can shield variables that contain classes, whereas Lambda expressions do not.
    Yes (they can cause compilation errors), such as the following code:

    int a = 10; 
    Runnable r1 = () -> { 
    int a = 2; //Class contains variable a
    System.out.println(a); 
    };
  • For functional interfaces with the same parameters, calls will result in Lambda-compliant results, but NetBeans and IntelliJ both support this refactoring, which can automatically help you check to avoid these problems.

3. Conversion from Lambda expression to method reference

Dinners are categorized according to the calorie level of food:

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 
 menu.stream() 
 .collect( 
 groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
 else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; 
 else return CaloricLevel.FAT; 
 }));

You can extract the content of a Lambda expression into a separate method and pass it as a parameter to groupingBy
Method. After transformation, the code becomes more concise and the intention of the program becomes clearer:

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = 
 menu.stream().collect(groupingBy(Dish::getCaloricLevel)); 

To implement this solution, you also need to add the getCaloricLevel method to the Dish class:

public class Dish{ 
 ... 
 public CaloricLevel getCaloricLevel(){ 
 if (this.getCalories() <= 400) return CaloricLevel.DIET; 
 else if (this.getCalories() <= 700) return CaloricLevel.NORMAL; 
 else return CaloricLevel.FAT; 
 } 
}

  • In addition, we should try our best to consider using static assistant methods, such as comparing, maxBy.
 inventory.sort( 
 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); 

 inventory.sort(comparing(Apple::getWeight));
  • The sum or maximum can be easily obtained by using Collectors interface, which is much more intuitive than using Lambada expression and the underlying reduction operation.
int totalCalories = 
  menu.stream().map(Dish::getCalories) 
  .reduce(0, (c1, c2) -> c1 + c2);

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

4. Switching from imperative data processing to Stream

Original:

List<String> dishNames = new ArrayList<>(); 
for(Dish dish: menu){ 
 if(dish.getCalories() > 300){ 
 dishNames.add(dish.getName()); 
 } 
}

Replace with streaming:

menu.parallelStream() 
 .filter(d -> d.getCalories() > 300) 
 .map(Dish::getName) 
 .collect(toList());

5. Increase code flexibility

(1) Using Functional Interface

The flexibility brought about by Lambda expressions is conditional delayed execution and surround execution.

(2) Conditional delay in execution

If you find that you need to frequently query the state of an object from the client code, just to pass parameters and call a method of the object (such as output a log), consider implementing a new method, using Lambda or method expression as parameters, which calls the original method after checking the state of the object.

(3) Circumferential execution

If you find that your business code is very different, but they have the same preparation and cleaning stages, then you can use Lambda to implement this part of the code. The advantage of this approach is that you can reuse the logic of the preparation and cleanup phases and reduce duplication of redundant code.

String oneLine = 
 processFile((BufferedReader b) -> b.readLine()); 
String twoLines = 
 processFile((BufferedReader b) -> b.readLine() + b.readLine()); 
public static String processFile(BufferedReaderProcessor p) throws 
 IOException { 
 try(BufferedReader br = new BufferedReader(new FileReader("java8inaction/ 
 chap8/data.txt"))){ 
 return p.process(br); 
 } 
} 
public interface BufferedReaderProcessor{ 
 String process(BufferedReader b) throws IOException; 
}

2. Reconstructing Object-Oriented Design Patterns with Lambda

1. Strategic model

Policy patterns represent generic solutions to a class of algorithms, and you can choose which solution to use at runtime.
Number). You can start by defining an interface for validating text (expressed as String):

public interface ValidationStrategy { 
  boolean execute(String s); 
} 

Secondly, you define one or more specific implementations of the interface:

public class IsAllLowerCase implements ValidationStrategy { 
  public boolean execute(String s){ 
  return s.matches("[a-z]+"); 
  } 
} 
public class IsNumeric implements ValidationStrategy { 
  public boolean execute(String s){ 
  return s.matches("\\d+"); 
  } 
} 

Then you can use these slightly different verification strategies in your program:

public class Validator{ 
  private final ValidationStrategy strategy; 
  public Validator(ValidationStrategy v){ 
  this.strategy = v;
  } 
  public boolean validate(String s){ 
  return strategy.execute(s); 
  } 
} 
Validator numericValidator = new Validator(new IsNumeric()); 
boolean b1 = numericValidator.validate("aaaa"); 
Validator lowerCaseValidator = new Validator(new IsAllLowerCase ()); 
boolean b2 = lowerCaseValidator.validate("bbbb"); 

If Lambda expression is used, then:

Validator numericValidator = 
 new Validator((String s) -> s.matches("[a-z]+")); 
boolean b1 = numericValidator.validate("aaaa"); 
Validator lowerCaseValidator = 
 new Validator((String s) -> s.matches("\\d+")); 
boolean b2 = lowerCaseValidator.validate("bbbb");

2. Template method

If you need to adopt the framework of an algorithm and hope to have some flexibility to improve some parts of it, then the template design pattern is a more general scheme.

abstract class OnlineBanking { 
 public void processCustomer(int id){ 
 Customer c = Database.getCustomerWithId(id); 
 makeCustomerHappy(c); 
 } 
 abstract void makeCustomerHappy(Customer c); 
} 

ProceCustomer method builds the framework of online banking algorithm: get the ID provided by customers, and then provide services to satisfy users. Different branches can provide differentiated implementations of this method by inheriting the OnlineBanking class.
If Lambda expression is used:

public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){ 
 Customer c = Database.getCustomerWithId(id); 
 makeCustomerHappy.accept(c); 
} 

new OnlineBankingLambda().processCustomer(1337, (Customer c) -> 
 System.out.println("Hello " + c.getName());

3. Observer model

Examples: Several newspaper organizations, such as the New York Times, the Guardian and Le Monde, subscribe to news, hoping to be notified when they receive news containing keywords of interest.

interface Observer { 
 void notify(String tweet); 
}
class NYTimes implements Observer{ 
 public void notify(String tweet) { 
 if(tweet != null && tweet.contains("money")){ 
 System.out.println("Breaking news in NY! " + tweet); 
 } 
 } 
} 
class Guardian implements Observer{ 
 public void notify(String tweet) { 
 if(tweet != null && tweet.contains("queen")){ 
 System.out.println("Yet another news in London... " + tweet); 
 } 
 } 
} 
class LeMonde implements Observer{ 
 public void notify(String tweet) { 
 if(tweet != null && tweet.contains("wine")){ 
 System.out.println("Today cheese, wine and news! " + tweet); 
 } 
 } 
}
interface Subject{ 
 void registerObserver(Observer o); 
 void notifyObservers(String tweet); 
}
class Feed implements Subject{ 
 private final List<Observer> observers = new ArrayList<>(); 
 public void registerObserver(Observer o) { 
 this.observers.add(o); 
 } 
 public void notifyObservers(String tweet) { 
 observers.forEach(o -> o.notify(tweet)); 
 } 
}
Feed f = new Feed(); 
f.registerObserver(new NYTimes()); 
f.registerObserver(new Guardian()); 
f.registerObserver(new LeMonde()); 
f.notifyObservers("The queen said her favourite book is Java 8 in Action!");

With Lambda expressions, you don't need to explicitly instantiate three observer objects, but simply pass Lambda expressions to represent the actions that need to be performed:

f.registerObserver((String tweet) -> { 
 if(tweet != null && tweet.contains("money")){ 
 System.out.println("Breaking news in NY! " + tweet); 
 } 
}); 
f.registerObserver((String tweet) -> { 
 if(tweet != null && tweet.contains("queen")){ 
 System.out.println("Yet another news in London... " + tweet); 
 } 
});

4. Responsibility Chain Model

The responsibility chain pattern is a general scheme for creating a sequence of processing objects, such as operation sequences. A processing object may need to pass the result to another object after some work has been done. The object then does some work and then forwards it to the next processing object, and so on. Usually, this pattern is implemented by defining an abstract class representing the processing object, in which a field is defined to record subsequent objects. Once an object completes its work, the processing object transfers its work to its successors.

public abstract class ProcessingObject<T> { 
 protected ProcessingObject<T> successor; 
 public void setSuccessor(ProcessingObject<T> successor){ 
 this.successor = successor; 
 } 
 public T handle(T input){ 
 T r = handleWork(input); 
 if(successor != null){ 
 return successor.handle(r); 
 } 
 return r; 
 } 
 abstract protected T handleWork(T input); 
}
public class HeaderTextProcessing extends ProcessingObject<String> { 
 public String handleWork(String text){ 
 return "From Raoul, Mario and Alan: " + text; 
 } 
} 
public class SpellCheckerProcessing extends ProcessingObject<String> { 
 public String handleWork(String text){ 
 return text.replaceAll("labda", "lambda"); 
 } 
}
ProcessingObject<String> p1 = new HeaderTextProcessing(); 
ProcessingObject<String> p2 = new SpellCheckerProcessing(); 
p1.setSuccessor(p2);//Link two processing objects together
String result = p1.handle("Aren't labdas really sexy?!!"); 
System.out.println(result); 

Using Lambda expressions
You can treat an object as an instance of a function, or, more precisely, as an instance of UnaryOperator < String >. To link these functions, you need to construct them using the and then method.

UnaryOperator<String> headerProcessing = 
 (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing = 
 (String text) -> text.replaceAll("labda", "lambda"); 
Function<String, String> pipeline = 
 headerProcessing.andThen(spellCheckerProcessing); 
String result = pipeline.apply("Aren't labdas really sexy?!!");

5. Factory Model

public class ProductFactory { 
  public static Product createProduct(String name){ 
  switch(name){ 
  case "loan": return new Loan(); 
  case "stock": return new Stock(); 
  case "bond": return new Bond(); 
  default: throw new RuntimeException("No such product " + name); 
  } 
  } 
}

Product p = ProductFactory.createProduct("loan");

Using Lambda expressions
In Chapter 3, we already know that constructors can be referenced as reference methods. For example, here's a reference loan
(Loan) Examples of constructors:

The constructor parameter list should be consistent with the abstract method parameter list in the interface! Therefore, if there are more than one parameter in the constructor, you need to customize the functional interface.

Supplier<Product> loanSupplier = Loan::new; 
Loan loan = loanSupplier.get(); 

In this way, you can reconstruct the previous code, create a Map, and map the product name to the corresponding constructor:

final static Map<String, Supplier<Product>> map = new HashMap<>(); 
static { 
 map.put("loan", Loan::new); 
 map.put("stock", Stock::new); 
 map.put("bond", Bond::new); 
} 

Now, you can use this Map to instantiate different products as you used to do with factory design patterns.

public static Product createProduct(String name){ 
 Supplier<Product> p = map.get(name); 
 if(p != null) return p.get(); 
 throw new IllegalArgumentException("No such product " + name); 
}

3. Testing Lambda expressions

  • You can access Lambda functions with a field
  • Testing methods that use Lambda expressions
  • One strategy is to convert Lambda expressions into method references and then follow the usual pattern
  • The method of accepting a function as a parameter or returning a function (so-called higher-order function, which we'll cover in depth in Chapter 14) is more difficult to test. If a method accepts Lambda expressions as parameters, one solution you can take is to test it using different Lambda expressions.

The equals method of List is mentioned in this paper.
Both ArrayList and Vector implement List interface and inherit AbstractList Abstract class. The equals method is defined in AbstractList class. The source code is as follows:

public boolean equals(Object o) {    
  if (o == this)        
     return true;    
  // Determine if it is a List List, as long as the List interface is implemented, it is a List List?
  if (!(o instanceof List))        
     return false;    
  // Traverse all elements of list
  ListIterator<E> e1 = listIterator();
  ListIterator e2 = ((List) o).listIterator();    
  while (e1.hasNext() && e2.hasNext()) {
      E o1 = e1.next();
      Object o2 = e2.next();        
      // Withdrawal if there are inequalities
      if (!(o1==null ? o2==null : o1.equals(o2)))            
          return false;
   }    
   // Is the length equal?
   return !(e1.hasNext() || e2.hasNext());

As can be seen from the source code, the equals method does not care about the specific implementation class of List. As long as it implements the List interface, and all elements are equal and the length is equal, it shows that the two Lists are equal, so the example will return true.

Four, debugging

1. View stack trace

Because Lambda expressions have no names, their stack tracing may be difficult to analyze, and the compiler can only specify a name for them. If you use a large number of classes and contain multiple Lambda expressions, this becomes a very headache. This is one aspect that future versions of the Java compiler can improve.

2. Debugging with logs

This is the time when peek, the streaming method, has come to the fore. Peek is designed to insert and execute an action before each element of the flow can resume running. But instead of restoring the entire flow as forEach did, it only succeeds to the next operation in the pipeline when the operation is completed on one element.

List<Integer> numbers = Arrays.asList(2, 3, 4, 5);

List<Integer> result = 
 numbers.stream() 
 .peek(x -> System.out.println("from stream: " + x))
//Output current element values from data sources
 .map(x -> x + 17) 
 .peek(x -> System.out.println("after map: " + x)) 
//Output map operation results
 .filter(x -> x % 2 == 0) 
 .peek(x -> System.out.println("after filter: " + x))
//After the output is filter ed, the number of elements remaining
 .limit(3) 
 .peek(x -> System.out.println("after limit: " + x))
//After the output is limit ed, the number of elements remaining
 .collect(toList());

Output results:

from stream: 2 
after map: 19 
from stream: 3 
after map: 20 
after filter: 20 
after limit: 20 
from stream: 4 
after map: 21 
from stream: 5 
after map: 22 
after filter: 22 
after limit: 22

Posted by HavokDelta6 on Sun, 28 Apr 2019 23:50:35 -0700