Java 8:1行为参数化

164 次查看

行为参数化本质上是一块代码并使其可用而不执行它。例如,它可以传递给方法。由于Java 8引入了lambdas(最后),现在可以使用匿名函数来参数化方法的行为。如果您熟悉Javascript,Scala,Groovy或内置lambdas的任何其他语言,那么您可能一直在使用行为参数化。

在软件开发中,用户需求可能会发生变化,这可能不会让您惊讶。将行为作为参数传递可以帮助减轻变化的痛苦。

不幸的是,有些应用程序无法升级以与最新版本的Java一起运行。因此,我将介绍可用于Java 8之前的运行时的替代解决方案。在本文中,我将从如何使用以前的Java版本实现行为参数化的示例开始,然后将这些解决方案与lambdas进行比较。在这个过程中,我试图展示函数式编程的成语如何使您的生活更容易作为软件开发人员。

示例域

我们来看一个过滤Java对象的例子。更具体地说,我将使用Java 7过滤book对象列表而不使用任何外部库。该书类有3个领域:namepageCountauthor。想象一下,我们有一个图书馆应用程序,根据要求,应该可以找到超过200页的书籍。

public class Book {
    private String name;
    private int pageCount;
    private String author;

    //getters and setters omitted...
}

找到长篇小说的简单方法(在我们的案例中是一本长篇小说是一本超过200页的书)是循环遍历书籍列表,使用if子句来检查它是否超过指定数量的页面,添加书到结果列表,最后还给它。

public static List<Book> findLongNovels(List<Book> books) {
    List<Book> result = new ArrayList<>();
    for (Book book : books) {
        if (book.getPageCount() > 200) {
            result.add(book);
        }
    }
    return result;
}

改变要求

一切都很好,花花公子,对吧?按照惯例,需求会发生变化,并且会增加新的要求。现在,库应用程序应该能够按作者过滤书籍。完成非常简单。只需使用与以前相同的总体布局。

public static List<Book> filterBooksByAuthor(List<Book> books, String author) {
    List<Book> result = new ArrayList<>();
    for (Book book : books) {
        if (author.equals(book.getAuthor())) {
            result.add(book);
        }
    }
    return result;
}

代码重用

如果filterBooksByAuthor与之前的findLongNovels方法进行比较,您可以清楚地看到它们非常相似。这是一个WET解决方案。让我们干涸吧。整体结构是一样的。代码遍历书籍列表并应用过滤子句。目标是保持迭代和过滤分离。使用Java 7时,我们可以创建一个BookPredicate可以定义过滤逻辑的接口。一个谓语本质上是一个布尔值函数。由于Java 7没有lambdas,我们将把谓词包装在一个类中。

public interface BookPredicate {
    boolean test(Book book);
}

可以将过滤逻辑移动到实现该BookPredicate接口的单独类。

public class LengthPredicate implements BookPredicate {
    private int length;

    public LengthPredicate(int length) {
        this.length = length;
    }

    @Override
    public boolean test(Book book) {
        return book.getPageCount() > length;
    }
}

public class AuthorPredicate implements BookPredicate {
    private String author;

    public AuthorPredicate(String author) {
        this.author = author;
    }

    @Override
    public boolean test(Book book) {
        return author.equals(book.getAuthor());
    }
}

在进行一些重构之后,可以重用迭代书籍列表的方法。

public static List<Book> filterBooks(List<Book> books, BookPredicate p) {
    List<Book> result = new ArrayList<>();
    for (Book book : books) {
        if (p.test(book)) {
            result.add(book);
        }
    }
    return result;
}

//somewhere in main
BookPredicate lengthPredicate = new LengthPredicate(200);
BookPredicate authorPredicate = new AuthorPredicate("Lewis Carrol");
filterBooks(books, lengthPredicate);
filterBooks(books, authorPredicate);

太冗长了?

现在我们不是在重复自己,但是嘿,这就是要编写的代码。正如他们所说,Java是冗长的。最初有2种方法可以过滤书籍。那是大约15行代码。删除重复的代码并将过滤逻辑移到单独的类后,有超过30行。虽然这对于一个小项目来说并不多,但是对于一个大型项目来说,这些线条会加起来。有什么办法可以写出更简洁的代码吗?

匿名内部类

我们不是定义a的具体实现,而是动态BookPredicate创建一个。

filterBooks(books, new BookPredicate() {
    @Override
    public boolean test(Book book) {
        return "Lewis Carrol".equals(book.getAuthor());
    }
});

这很简洁。它看起来几乎像一个lambda。事实上,在使用Java 8时,IDE会建议您用lambda替换它。匿名内部类的缺点是它带有样板代码。需要实例化一个新对象,需要覆盖一个方法,并在这里和那里使用一些花括号。该样板使得更难以专注于实际重要的部分 – test方法内部的比较。

使用第三方库

正如所料,创建库是为了克服语言的缺点。鲍勃叔叔在他的博客文章中写道,我们编写框架来弥补我们希望用我们的语言缺少的功能。你见过的每个框架都只是这句话的回声:

我的语言很糟糕!

有什么替代品呢?Google Guava库具有允许您进行更多功能样式编程的谓词。

Iterables.filter(books, new Predicate<Book>() {
    @Override
    public boolean apply(Book input) {
        return "Lewis Carrol".equals(input.getAuthor());
    }
});

它与我们用filterBooks方法实现的非常相似。在函数式编程中,通过将谓词应用于列表的每个元素来完成对项列表的过滤。Filter是函数式语言的常用功能。稍后我们将看到Java 8也包含它。使用Guava的好处是您不必编写列表迭代代码和谓词接口。

另一种可能的解决方案是将lambdaj与Hamcrest匹配器一起使用。lambdaj是一个库,允许您以伪功能和静态类型的方式操作集合。

filter(having(on(Book.class).getAuthor(), equalTo("Lewis Carrol")), books);

哇,这一切都在一条线上。如果有一个更复杂的过滤条款,这将变得有点麻烦。

Java 8 lambdas

最新版本带来了一些新功能,可以提高代码的可读性,并帮助语言在未来保持竞争力。让我们看看书籍过滤示例,看看行为参数化如何与语言中内置的lambdas一起使用。

首先,我们需要重写filterBooks要使用的方法,java.util.function.Predicate这是Java 8中的新接口。

public static List<Book> filterBooks(List<Book> books, Predicate<Book> p) {
    List<Book> result = new ArrayList<>();
    for (Book book : books) {
        if (p.test(book)) {
            result.add(book);
        }
    }
    return result;
}

在调用时filterBooks,我们可以传递一个lambda表达式,告诉它​​如何进行过滤。

filterBooks(books, book -> "Lewis Carrol".equals(book.getAuthor()));

虽然我们使用了一个lambda表达式并使filterBooks方法的行为可参数化,但仍然有这个样板代码迭代一系列书籍。以前我提到Java 8包含了函数式语言中常用的过滤器习语。Streams是一种新的API,有助于表达复杂的数据处理查询。其中,它包括过滤方法。

books.stream().filter(b -> "Lewis Carrol".equals(b.getAuthor())).collect(toList());

可以看出,书籍列表没有传递给方法,但我们可以filter通过首先从中创建流来调用方法。迭代由Streams API处理,由于lambda,行为是可参数化的。因此,Java 8不是编写大量的样板代码,而是处理常见的任务,只需一行代码即可解决手头的问题。

还记得那些不断变化

在这篇文章的开头,我给出了一个改变需求的例子。现在可以使用lambda了,让我们看看库应用程序如何处理新的功能请求。应该可以找到超过200页的书籍。

books.stream().filter(b -> b.getPageCount() > 200).collect(toList());

在不修改任何现有代码的情况下,使用新行为过滤书籍列表非常容易。

Retrolambda

如果您使用的是以前版本的Java,那么您仍然可以通过使用Retrolambda来利用lambdas 。它允许您在Java 7,6或5上运行带有lambda表达式方法引用try-with-resources语句的Java 8代码。它通过转换Java 8编译的字节码来实现,以便它可以在较旧的Java运行时上运行。我不是其内部工作的专家,但从我所读到的,它取代了lambdas与匿名的内部类。

尽管如此,Retrolambda并没有向后传输Streams API。为此,您可以使用streamsupport。

摘要

使用函数式编程中常用的习语可以极大地提高代码的可读性。行为参数化很好,因为它使您能够将迭代集合的代码与应用于集合的每个元素的行为分开。这样可以更好地重用代码,并帮助您编写更灵活的API。

赞赏


微信赞赏

支付宝赞赏

java架构师历程,欢迎扫描关注