[Java 8] Using Lambda expressions for Design

Source: Internet
Author: User

[Java 8] Using Lambda expressions for Design
Use Lambda expressions for Design

In the previous articles, we have seen how Lambda expressions make code more compact and concise.

This article mainly introduces how Lambda expressions change program design and how to make programs more lightweight and concise. How to make the use of interfaces smoother and more intuitive.

Use Lambda expressions to implement policy Modes

Suppose there is an Asset type like this:

public class Asset {    public enum AssetType { BOND, STOCK };    private final AssetType type;    private final int value;    public Asset(final AssetType assetType, final int assetValue) {        type = assetType;        value = assetValue;    }    public AssetType getType() { return type; }    public int getValue() { return value; }}

Each asset has a type, which is represented by an enumeration type. At the same time, the asset also has its value, expressed in integer type.

If we want the value of all assets, Lambda can be easily implemented as follows:

public static int totalAssetValues(final List assets) {    return assets.stream()        .mapToInt(Asset::getValue)        .sum();}

Although the above Code can well complete the task of calculating the total assets, but careful analysis will find that this code puts the following three tasks together:

  1. How to traverse
  2. Computing
  3. How to calculate

    Now we have a new requirement to calculate the total value of the Bond type assets, so we will intuitively consider copying the previous code and then making targeted modifications:

    public static int totalBondValues(final List assets) {    return assets.stream()        .mapToInt(asset ->            asset.getType() == AssetType.BOND ? asset.getValue() : 0)        .sum();}

    The only difference is the Lambda expression passed into the mapToInt method. Of course, you can also add a filter before the mapToInt method to filter out unwanted Stock-type assets. In this way, the Lambda expression in mapToInt does not need to be modified.

    public static int totalBondValues(final List assets) {    return assets.stream()        .filter(asset -> asset.getType == AssetType.BOND)        .mapToInt(Asset::getValue)        .sum();}

    In this way, although new requirements are met, this practice obviously violates the DRY principle. We need to redesign them to enhance reusability. In the code for calculating Bond assets, Lambda expressions play two roles:

    1. How to traverse
    2. How to calculate

      When using object-oriented design, we will consider using the Strategy Pattern to separate the above two responsibilities. But here we use Lambda expressions for implementation:

      public static int totalAssetValues(final List assets, final Predicate assetSelector) {    return assets.stream().filter(assetSelector).mapToInt(Asset::getValue).sum();}

      The restructured method accepts the second parameter, which is a Predicate type functional interface. Obviously, it is used to specify how to traverse. This is actually a simple implementation of the Policy mode when using Lambda expressions, because Predicate expresses an action, and this action itself is a policy. This method is more lightweight, because it does not create additional interfaces or types, but only reuse the Predicate function interface provided in Java 8.

      For example, when we need to calculate the total value of all assets, the input Predicate can be like this:

      System.out.println("Total of all assets: " + totalAssetValues(assets, asset -> true));

      Therefore, after Predicate is used, the "How to traverse" task is also separated. Therefore, tasks are no longer entangled together, and the single responsibility principle is realized, which naturally improves reusability.

      Use Lambda expressions to implement Composition)

      In object-oriented design, it is generally considered that the combination method is better than the inheritance method, because it reduces unnecessary class layers. In fact, Lambda expressions can also be used for combination.

      For example, in the CalculateNAV class below, there is a method for calculating the stock value:

      public class CalculateNAV {    public BigDecimal computeStockWorth(final String ticker, final int shares) {        return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares));    }    //... other methods that use the priceFinder ...}

      Because the passed ticker is a string, it represents the stock code. In the calculation, we obviously need the stock price, so the priceFinder type is easily determined as Function. Therefore, we can declare the CalculateNAV constructor as follows:

      private Function
           
             priceFinder;public CalculateNAV(final Function
            
              aPriceFinder) {    priceFinder = aPriceFinder;}
            
           

      In fact, the above Code uses a design pattern called "Dependency Inversion Principle", and uses Dependency injection to associate types with specific implementations, instead of Directly Writing the implementation into the code, this improves the reusability of the Code.

      To test CalculateNAV, you can use JUnit:

      public class CalculateNAVTest {    @Test    public void computeStockWorth() {        final CalculateNAV calculateNAV = new CalculateNAV(ticker -> new BigDecimal("6.01"));        BigDecimal expected = new BigDecimal("6010.00");        assertEquals(0, calculateNAV.computeStockWorth("GOOG", 1000).compareTo(expected));    }    //...}

      Of course, you can also use a real Web Service to get the price of a stock:

      public class YahooFinance {    public static BigDecimal getPrice(final String ticker) {        try {            final URL url = new URL("http://ichart.finance.yahoo.com/table.csv?s=" + ticker);            final BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));            final String data = reader.lines().skip(1).findFirst().get();            final String[] dataItems = data.split(",");            return new BigDecimal(dataItems[dataItems.length - 1]);        } catch(Exception ex) {            throw new RuntimeException(ex);        }    }}

      In Java 8, BufferedReader also has a new method called lines to get a Stream object that contains all row data, obviously, this is also to make the class and functional programming better integrated.

      In addition, we want to explain the relationship between Lambda expressions and exceptions. Obviously, when using the getPrice method, we can directly pass in the method reference:YahooFinance::getPrice. But what if an exception occurs during this method call? When an exception occurs in the above Code, the exception is encapsulated into a RuntimeException and thrown again. This is because the Checked Exception can be thrown only when the methods in the function interface declare an Exception (that is, throws XXX ). Obviously, the apply method of the Function interface does not declare a checkable exception. Therefore, getPrice itself cannot throw a checkable exception, what we can do is encapsulate exceptions into runtime exceptions (non-inspected exceptions) and then throw them.

      Decorator Pattern)

      The decoration mode itself is not complex, but it is not easy to implement in the object-oriented design, because it requires many types to be designed and implemented, which undoubtedly increases the burden on developers. For example, there are various types of InputStream and OutputStream in JDK to decorate it, so the I/O-related types are designed to be too complicated, learning costs are high, so it is not easy to use them correctly and efficiently.

      Using Lambda expressions to implement the decoration mode is much easier. In the image field, filters are actually Decorator. We will add various filters to an image. The number of filters is uncertain, the order is also uncertain.

      For example, the following code is used to model the camera's color processing:

      @SuppressWarnings("unchecked")public class Camera {    private Function
           
             filter;    public Color capture(final Color inputColor) {        final Color processedColor = filter.apply(inputColor);        //... more processing of color...        return processedColor;    }    //... other functions that use the filter ...}
           

      Currently, only one filter is defined. We can use the compose and andThen of the Function to concatenate multiple functions (that is, filter:

      default 
           
             Function
            
              compose(Function
              before) {    Objects.requireNonNull(before);    return (V v) -> apply(before.apply(v));    }default 
             
               Function
              
                andThen(Function
                after) {    Objects.requireNonNull(after);    return (T t) -> after.apply(apply(t));}
              
             
            
           

      We can find that the difference between the compose and andThen methods lies in the serial sequence. When compose is used, the input Function is called first; When andThen is used, the current Function is called first.

      Therefore, in the Camera type, we can define a method to concatenate an indefinite number of Filters:

      public void setFilters(final Function
           
            ... filters) {    filter = Arrays.asList(filters).stream()        .reduce((current, next) -> current.andThen(next))        .orElse(color -> color);}
           

      As mentioned above, because the reduce method returns an object of the Optional type, special processing is required when the result does not exist. The preceding orElse method is called to obtain an alternative when the result does not exist. When the setFilters method does not accept any parameters, orElse will be called,color -> colorIs to directly return the color without any operation.

      In fact, the Function interface also defines a static method identity to process the scenario in which the user needs to directly return the user:

      static 
           
             Function
            
              identity() {    return t -> t;}
            
           

      Therefore, you can improve the implementation of the setFilters method as follows:

      public void setFilters(final Function
           
            ... filters) {    filter = Arrays.asList(filters).stream()        .reduce((current, next) -> current.andThen(next))        .orElseGet(Function::identity);}
           

      OrElse is replaced with orElseGet, which is defined as follows:

      public T orElse(T other) {    return value != null ? value : other;}public T orElseGet(Supplier
            other) {    return value != null ? value : other.get();}

      The former directly returns the input parameter other when the value is null, while the latter gets the object to be returned by calling the get method in Supplier. Here, a new functional interface Supplier appears:

      @FunctionalInterfacepublic interface Supplier
           
             {    T get();}
           

      It directly returns the required object without any parameters. This is similar to the non-argument constructor of A Class (also called a factory in some cases.

      When we talk about Supplier, we can't help but mention the Consumer function interfaces that correspond to it. They are just a complementary relationship. The accept method in Consumer accepts a parameter but does not return a value.

      Now we can use the filter function of Camera:

      final Camera camera = new Camera();final Consumer
           
             printCaptured = (filterInfo) ->    System.out.println(String.format("with %s: %s", filterInfo,        camera.capture(new Color(200, 100, 200))));camera.setFilters(Color::brighter, Color::darker);printCaptured.accept("brighter & darker filter");
           

      Unconsciously, we implemented a lightweight decoration mode (Decorator Pattern) in the setFilters method, without defining any redundant types. We only need to use Lambda expressions.

      Understand the default method of the interface

      In Java 8, interfaces can also have non-abstract methods, which is a very important design. From the perspective of the Java compiler, the parsing of the default method has the following rules:

      1. The child type automatically has the default method in the parent type.
      2. For the default method implemented in the interface, the sub-type of the interface can overwrite the method.
      3. The specific implementation and abstract declaration in the class will overwrite the default methods in all implementation interface types.
      4. When there is a conflict between two or more default methods, the implementation class needs to resolve this conflict.

        The preceding rules are described as follows:

        public interface Fly {    default void takeOff() { System.out.println("Fly::takeOff"); }    default void land() { System.out.println("Fly::land"); }    default void turn() { System.out.println("Fly::turn"); }    default void cruise() { System.out.println("Fly::cruise"); }}public interface FastFly extends Fly {    default void takeOff() { System.out.println("FastFly::takeOff"); }}public interface Sail {    default void cruise() { System.out.println("Sail::cruise"); }    default void turn() { System.out.println("Sail::turn"); }}public class Vehicle {    public void turn() { System.out.println("Vehicle::turn"); }}

        For rule 1, the FastFly and Fly interfaces can be described. Although FastFly overwrites the takeOff method in Fly, it also inherits the other three methods in Fly.

        For rule 2, if any interface inherits FastFly or any type implements FastFly, the takeOff method in the Child type comes from FastFly instead of the Fly interface.

        public class SeaPlane extends Vehicle implements FastFly, Sail {    private int altitude;    //...    public void cruise() {        System.out.print("SeaPlane::cruise currently cruise like: ");        if(altitude > 0)            FastFly.super.cruise();        else            Sail.super.cruise();    }}

        The preceding SeaPlane inherits the Vehicle type and implements both the FastFly and Sail interfaces. Because both interfaces define the cruise method, conflicts may occur according to Rule 4. Therefore, SeaPlane is needed to solve this conflict, and the solution is to redefine this method. However, methods in the parent type can still be called. As the code above will first judge the height and then call the method of the corresponding parent class.

        In fact, the turn method also exists in the FastFly and Sail types. This method does not conflict because the Vehicle method overwrites the default method in the interface. Therefore, according to rule 3, the turn method does not conflict.

        Some people may think that in Java 8, the interface type is more like an abstract class, because it can not only have abstract methods, but also have specific implementations. However, this statement is too one-sided. After all, the interface still cannot possess the State, while the abstract class can hold the State. At the same time, from the inheritance point of view, a class can implement multiple interfaces, and a class can only inherit one class at most.

        Use Lambda expressions to create smoother APIs

        For example, there is a type for sending mail and Its Usage:

        public class Mailer {    public void from(final String address) { /*... */ }    public void to(final String address) { /*... */ }    public void subject(final String line) { /*... */ }    public void body(final String message) { /*... */ }    public void send() { System.out.println("sending..."); }    //...}Mailer mailer = new Mailer();mailer.from("build@agiledeveloper.com");mailer.to("venkats@agiledeveloper.com");mailer.subject("build notification");mailer.body("...your code sucks...");mailer.send();

        This kind of code is almost encountered every day. But did you find the bad smell? Do you think the mailer instance appears too frequently? In this case, we can use the Method Chaining Pattern to improve:

        public class MailBuilder {    public MailBuilder from(final String address) { /*... */; return this; }    public MailBuilder to(final String address) { /*... */; return this; }    public MailBuilder subject(final String line) { /*... */; return this; }    public MailBuilder body(final String message) { /*... */; return this; }    public void send() { System.out.println("sending..."); }    //...}new MailBuilder()    .from("build@agiledeveloper.com")    .to("venkats@agiledeveloper.com")    .subject("build notification")    .body("...it sucks less...")    .send();

        This is much smoother and the code is more concise. However, the above Code still has problems:

        1. The use of the new keyword adds noise.
        2. The reference of the newly created object can be saved, meaning that the lifecycle of the object is unpredictable.

          This time Lambda was also involved in the improvement process:

          public class FluentMailer {    private FluentMailer() {}    public FluentMailer from(final String address) { /*... */; return this; }    public FluentMailer to(final String address) { /*... */; return this; }    public FluentMailer subject(final String line) { /*... */; return this; }    public FluentMailer body(final String message) { /*... */; return this; }    public static void send(final Consumer
                   
                     block) {        final FluentMailer mailer = new FluentMailer();        block.accept(mailer);        System.out.println("sending...");    }    //...}FluentMailer.send(mailer ->    mailer.from("build@agiledeveloper.com")        .to("venkats@agiledeveloper.com")        .subject("build notification")        .body("...much better..."));
                   

          The most important improvement is to declare the type of constructor as private, which prevents explicit instantiation outside the class and controls the life cycle of the instance. Then the send method is restructured into a static method, which accepts a Consumer type Lambda expression to complete operations on the newly created instance.

          At this point, the life cycle of the newly created instance is very clear: that is, when the send method call ends, the life cycle of the Instance ends. This Pattern is also intuitively called "Loan Pattern ". Obtain it, operate it, and return it.

          Exception Handling

          As mentioned earlier, if the method of the function interface does not define a checkable exception that can be thrown, it cannot be used to handle possible checkable exceptions, for example, the typical IOException type must be handled during file operations:

          Public class HandleException {public static void main (String [] args) throws IOException {List
                   
                    
          Paths = Arrays. asList ("/usr", "/tmp"); paths. stream (). map (path-> new File (path ). getCanonicalPath ()). forEach (System. out: println); // the above Code cannot be compiled because it does not handle possible IOException }}//... unreported exception IOException; must be caught or declared to be thrown //. map (path-> new File (path ). getCanonicalPath () // ^
                   

          This is because the Function interface required by the map method does not declare any checkable exceptions that can be thrown. Here we have two options:

          1. Handle checked exceptions in Lambda expressions
          2. Capture the checked exception and throw it again in the form of a non-checked exception (such as RuntimeException)

            When using the first choice, the Code is as follows:

            paths.stream()    .map(path -> {        try {            return new File(path).getCanonicalPath();        } catch(IOException ex) {            return ex.getMessage();        }    })    .forEach(System.out::println);

            When using the second choice, the Code is as follows:

            paths.stream()    .map(path -> {        try {            return new File(path).getCanonicalPath();        } catch(IOException ex) {            throw new RuntimeException(ex);        }    })    .forEach(System.out::println);

            In a single-threaded environment, it is feasible to capture the checked exception and re-Throw the non-checked exception. However, in a multi-threaded environment, there are some risks.

            In a multi-threaded environment, errors in Lambda expressions are automatically transmitted to the main thread. This brings about two problems:

            1. This does not stop other Lambda expressions that are being executed in parallel.
            2. If multiple threads throw an exception, only one exception can be caught in the main thread. If the exception information is important, a better way is to handle the exception in the Lambda expression and return the exception information as part of the result to the main thread.

              In fact, there is another way. Define our own function Interfaces Based on Exception Handling needs, such:

              @FunctionalInterfacepublic interface UseInstance
                           
                             {    void accept(T instance) throws X;}
                           

              In this way, any Lambda expression using UseInstance type can throw various exceptions.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.