【问题标题】:Converting array iteration to lambda function using Java8使用 Java8 将数组迭代转换为 lambda 函数
【发布时间】:2019-10-13 10:32:44
【问题描述】:

我正在尝试转换为 Lambda 函数

到目前为止,我能够将上面的代码转换为如下所示的 lambda 函数

Stream.of(acceptedDetails, rejectedDetails)
.filter(list -> !isNull(list) && list.length > 0)
.forEach(new Consumer<Object>() {
    public void accept(Object acceptedOrRejected) {
        String id;
        if(acceptedOrRejected instanceof EmployeeValidationAccepted) {
            id = ((EmployeeValidationAccepted) acceptedOrRejected).getId();
        } else {
            id = ((EmployeeValidationRejected) acceptedOrRejected).getAd().getId();
        }

        if(acceptedOrRejected instanceof EmployeeValidationAccepted) {
            dates1.add(new Integer(id.split("something")[1]));
            Integer empId = Integer.valueOf(id.split("something")[2]);
            empIds1.add(empId);
        } else {
            dates2.add(new Integer(id.split("something")[1]));
            Integer empId = Integer.valueOf(id.split("something")[2]);
            empIds2.add(empId);
        }
    }
});

但我的目标仍然是避免重复相同的逻辑并转换为 Lambda 函数,仍然在我转换的 lambda 函数中我觉得它不干净和高效。

这只是为了我的学习方面,我通过一个现有的代码 sn-p 来做这些事情。

谁能告诉我如何即兴转换转换后的 Lambda 函数

【问题讨论】:

  • 这是一个相当大的例子,下面的代码会检查命令式没有的 instanceof,你的目标到底是什么?对我来说,使用 instanceof 似乎很糟糕
  • 如果您的应用程序的这一部分对性能不敏感(就像真的很敏感),我会为empIdAccepted 做一个stream.map() 等,为dateAccepted 做一个等等更清晰,并将您显示的逻辑分成彼此不相关的不同方面。这是函数式编程的基础:分解成子操作或步骤
  • 我的目标是在不重复任何代码或逻辑的情况下转换为 lambda 函数
  • 函数式编程部分是关于一种更细粒度的方法,在这种方法中,您将工作分块/划分为多个步骤 - 当您停止实现自己完成所有事情的函数时,重复代码和/或步骤是给定的(例如空值检查 + 转换而不是一件事)
  • 在您的filter 步骤中,流元素似乎是数组(您正在访问list.length),然后,在forEach 步骤中,它们突然应该是EmployeeValidationAccepted 的实例或EmployeeValidationRejected。这是行不通的。用这个不完整的损坏代码替换您的工作原始代码也没有建设性。在原始代码中,您有 dateRejectedempIdRejected,但现在您有 dates1dates2empIds1empIds2,它们都没有声明。

标签: java lambda java-8 functional-programming functional-interface


【解决方案1】:

通常,当您尝试重构代码时,您应该只关注必要的更改。

仅仅因为您将使用 Stream API,没有理由用检查 null 或不在基于循环的代码中的空数组来混淆代码。您也不应该将BigInteger 更改为Integer

然后,您有两个不同的输入,并希望从每个输入中获得不同的结果,换句话说,您有两个完全不同的操作。虽然考虑在它们之间共享公共代码是合理的,但一旦您确定了相同的代码,尝试将两个完全不同的操作表示为一个操作是没有意义的。

首先,让我们看看我们如何为传统循环执行此操作:

static void addToLists(String id, List<Integer> empIdList, List<BigInteger> dateList) {
    String[] array = id.split("-");
    dateList.add(new BigInteger(array[1]));
    empIdList.add(Integer.valueOf(array[2]));
}
List<Integer> empIdAccepted = new ArrayList<>();
List<BigInteger> dateAccepted = new ArrayList<>();

for(EmployeeValidationAccepted acceptedDetail : acceptedDetails) {
    addToLists(acceptedDetail.getId(), empIdAccepted, dateAccepted);
}

List<Integer> empIdRejected = new ArrayList<>();
List<BigInteger> dateRejected = new ArrayList<>();

for(EmployeeValidationRejected rejectedDetail : rejectedDetails) {
    addToLists(rejectedDetail.getAd().getId(), empIdRejected, dateRejected);
}

如果我们想表达与 Stream 操作相同的内容,则存在每个操作有两个结果的障碍。直到 JDK 12 才真正获得内置解决方案:

static Collector<String,?,Map.Entry<List<Integer>,List<BigInteger>>> idAndDate() {
    return Collectors.mapping(s -> s.split("-"),
        Collectors.teeing(
            Collectors.mapping(a -> Integer.valueOf(a[2]), Collectors.toList()),
            Collectors.mapping(a -> new BigInteger(a[1]),  Collectors.toList()),
            Map::entry));
}
Map.Entry<List<Integer>, List<BigInteger>> e;
e = Arrays.stream(acceptedDetails)
        .map(EmployeeValidationAccepted::getId)
        .collect(idAndDate());

List<Integer> empIdAccepted = e.getKey();
List<BigInteger> dateAccepted = e.getValue();

e = Arrays.stream(rejectedDetails)
    .map(r -> r.getAd().getId())
    .collect(idAndDate());

List<Integer> empIdRejected = e.getKey();
List<BigInteger> dateRejected = e.getValue();

由于一个方法不能返回两个值,因此使用Map.Entry 来保存它们。

要将此解决方案与 JDK 12 之前的 Java 版本一起使用,您可以使用this answer 末尾发布的实现。然后,您还必须将 Map::entry 替换为 AbstractMap.SimpleImmutableEntry::new

或者您使用为此特定操作编写的自定义收集器:

static Collector<String,?,Map.Entry<List<Integer>,List<BigInteger>>> idAndDate() {
    return Collector.of(
        () -> new AbstractMap.SimpleImmutableEntry<>(new ArrayList<>(), new ArrayList<>()),
        (e,id) -> {
            String[] array = id.split("-");
            e.getValue().add(new BigInteger(array[1]));
            e.getKey().add(Integer.valueOf(array[2]));
        },
        (e1, e2) -> {
            e1.getKey().addAll(e2.getKey());
            e1.getValue().addAll(e2.getValue());
            return e1;
        });
}

换句话说,使用 Stream API 并不总是让代码更简单。

最后一点,我们不需要使用 Stream API 来利用 lambda 表达式。我们还可以使用它们将循环移动到公共代码中。

static <T> void addToLists(T[] elements, Function<T,String> tToId,
                           List<Integer> empIdList, List<BigInteger> dateList) {
    for(T t: elements) {
        String[] array = tToId.apply(t).split("-");
        dateList.add(new BigInteger(array[1]));
        empIdList.add(Integer.valueOf(array[2]));
    }
}
List<Integer> empIdAccepted = new ArrayList<>();
List<BigInteger> dateAccepted = new ArrayList<>();
addToLists(acceptedDetails, EmployeeValidationAccepted::getId, empIdAccepted, dateAccepted);

List<Integer> empIdRejected = new ArrayList<>();
List<BigInteger> dateRejected = new ArrayList<>();
addToLists(rejectedDetails, r -> r.getAd().getId(), empIdRejected, dateRejected);

【讨论】:

    【解决方案2】:

    与@roookeee 已经发布的类似但可能稍微更简洁的方法是使用声明为的映射函数存储映射:

    Function<String, Integer> extractEmployeeId = empId -> Integer.valueOf(empId.split("-")[2]);
    Function<String, BigInteger> extractDate = empId -> new BigInteger(empId.split("-")[1]);
    

    然后继续映射:

    Map<Integer, BigInteger> acceptedDetailMapping = Arrays.stream(acceptedDetails)
            .collect(Collectors.toMap(a -> extractEmployeeId.apply(a.getId()),
                    a -> extractDate.apply(a.getId())));
    
    Map<Integer, BigInteger> rejectedDetailMapping = Arrays.stream(rejectedDetails)
            .collect(Collectors.toMap(a -> extractEmployeeId.apply(a.getAd().getId()),
                    a -> extractDate.apply(a.getAd().getId())));
    

    此后,您还可以访问与员工的employeeId 对应的接受或拒绝日期。

    【讨论】:

    • 另一个好主意!我的答案可以用Stream.concat 扩展,而不是收集两个变体,然后像你描述的那样过滤
    • 这假定 id 是真正唯一的并且顺序无关紧要。此外,它对每个元素执行两次split
    【解决方案3】:

    这个怎么样:

     class EmployeeValidationResult {
        //constructor + getters omitted for brevity
        private final BigInteger date;
        private final Integer employeeId;
    }
    
    List<EmployeeValidationResult> accepted = Stream.of(acceptedDetails)
        .filter(Objects:nonNull)
        .map(this::extractValidationResult)
        .collect(Collectors.toList());
    
    List<EmployeeValidationResult> rejected = Stream.of(rejectedDetails)
        .filter(Objects:nonNull)
        .map(this::extractValidationResult)
        .collect(Collectors.toList());
    
    
    EmployeeValidationResult extractValidationResult(EmployeeValidationAccepted accepted) {
        return extractValidationResult(accepted.getId());
    }
    
    EmployeeValidationResult extractValidationResult(EmployeeValidationRejected rejected) {
        return extractValidationResult(rejected.getAd().getId());
    }
    
    EmployeeValidationResult extractValidationResult(String id) {
        String[] empIdList = id.split("-");
        BigInteger date = extractDate(empIdList[1])
        Integer empId = extractId(empIdList[2]);
    
        return new EmployeeValidationResult(date, employeeId);
    }
    

    重复 filtermap 操作是一种很好的风格,并且可以明确地说明正在发生的事情。将两个对象列表合并为一个并使用instanceof 会使实现变得混乱,使其可读性/可维护性降低。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-02-06
      • 2020-08-08
      • 2015-05-30
      相关资源
      最近更新 更多