【问题标题】:Lambdas, multiple forEach with castingLambdas,多个 forEach 带强制转换
【发布时间】:2014-10-15 20:25:21
【问题描述】:

需要一些帮助,从我的 StackOverflow 杰出人物那里思考 lambda。

从一个列表中挑选一个列表的标准案例来收集图表深处的一些孩子。 Lambdas 有哪些很棒的方法可以帮助处理这个样板文件?

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<ContextInfo>();
    final StandardServer server = getServer();

    for (final Service service : server.findServices()) {
        if (service.getContainer() instanceof Engine) {
            final Engine engine = (Engine) service.getContainer();
            for (final Container possibleHost : engine.findChildren()) {
                if (possibleHost instanceof Host) {
                    final Host host = (Host) possibleHost;
                    for (final Container possibleContext : host.findChildren()) {
                        if (possibleContext instanceof Context) {
                            final Context context = (Context) possibleContext;
                            // copy to another object -- not the important part
                            final ContextInfo info = new ContextInfo(context.getPath());
                            info.setThisPart(context.getThisPart());
                            info.setNotImportant(context.getNotImportant());
                            list.add(info);
                        }
                    }
                }
            }
        }
    }
    return list;
}

请注意,列表本身将作为JSON 发送给客户端,因此不要关注返回的内容。一定是一些我可以减少循环的巧妙方法。

有兴趣看看我的专家同事创造了什么。鼓励采用多种方法。

编辑

findServices 和两个 findChildren 方法返回数组

编辑 - 奖励挑战

“不重要的部分”确实很重要。我实际上需要复制一个仅在 host 实例中可用的值。这似乎毁掉了所有美丽的例子。如何将国家发扬光大?

final ContextInfo info = new ContextInfo(context.getPath());
info.setHostname(host.getName()); // The Bonus Challenge

【问题讨论】:

标签: java lambda java-8 java-stream


【解决方案1】:

它的嵌套相当深,但似乎并不特别困难。

第一个观察结果是,如果 for 循环转换为流,则可以使用 flatMap 将嵌套的 for 循环“扁平化”为单个流。此操作采用单个元素并在流中返回任意数量的元素。我查了一下,发现StandardServer.findServices()返回了一个Service的数组,所以我们用Arrays.stream()把它变成了一个流。 (我对Engine.findChildren()Host.findChildren() 做了类似的假设。

接下来,每个循环中的逻辑执行instanceof 检查和强制转换。这可以使用流建模为filter 操作以执行instanceof,然后是map 操作,该操作简单地强制转换并返回相同的引用。这实际上是一个空操作,但它允许静态类型系统将 Stream&lt;Container&gt; 转换为 Stream&lt;Host&gt; 例如。

将这些转换应用于嵌套循环,我们得到以下结果:

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<ContextInfo>();
    final StandardServer server = getServer();

    Arrays.stream(server.findServices())
        .filter(service -> service.getContainer() instanceof Engine)
        .map(service -> (Engine)service.getContainer())
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .filter(possibleHost -> possibleHost instanceof Host)
        .map(possibleHost -> (Host)possibleHost)
        .flatMap(host -> Arrays.stream(host.findChildren()))
        .filter(possibleContext -> possibleContext instanceof Context)
        .map(possibleContext -> (Context)possibleContext)
        .forEach(context -> {
            // copy to another object -- not the important part
            final ContextInfo info = new ContextInfo(context.getPath());
            info.setThisPart(context.getThisPart());
            info.setNotImportant(context.getNotImportant());
            list.add(info);
        });
    return list;
}

但是等等,还有更多。

最后的forEach 操作是一个稍微复杂的map 操作,它将Context 转换为ContextInfo。此外,这些只是收集到List 中,因此我们可以使用收集器来执行此操作,而不是先创建和清空列表然后再填充它。应用这些重构会产生以下结果:

public List<ContextInfo> list() {
    final StandardServer server = getServer();

    return Arrays.stream(server.findServices())
        .filter(service -> service.getContainer() instanceof Engine)
        .map(service -> (Engine)service.getContainer())
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .filter(possibleHost -> possibleHost instanceof Host)
        .map(possibleHost -> (Host)possibleHost)
        .flatMap(host -> Arrays.stream(host.findChildren()))
        .filter(possibleContext -> possibleContext instanceof Context)
        .map(possibleContext -> (Context)possibleContext)
        .map(context -> {
            // copy to another object -- not the important part
            final ContextInfo info = new ContextInfo(context.getPath());
            info.setThisPart(context.getThisPart());
            info.setNotImportant(context.getNotImportant());
            return info;
        })
        .collect(Collectors.toList());
}

我通常会尽量避免使用多行 lambda(例如在最终的 map 操作中),因此我会将其重构为一个小辅助方法,该方法接受 Context 并返回 ContextInfo。这根本不会缩短代码,但我认为它确实使它更清晰。

更新

但是等等,还有更多。

让我们将对service.getContainer() 的调用提取到它自己的管道元素中:

    return Arrays.stream(server.findServices())
        .map(service -> service.getContainer())
        .filter(container -> container instanceof Engine)
        .map(container -> (Engine)container)
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        // ...

这暴露了instanceof 上的重复过滤,然后是带有强制转换的映射。总共进行了 3 次。似乎其他代码可能需要做类似的事情,所以最好将这部分逻辑提取到辅助方法中。问题是filter 可以更改流中元素的数量(删除不匹配的元素),但不能更改它们的类型。而map 可以改变元素的类型,但不能改变它们的数量。可以改变数量和类型吗?是的,又是我们的老朋友flatMap!所以我们的辅助方法需要获取一个元素并返回一个不同类型的元素流。该返回流将包含单个转换元素(如果它匹配)或者它将是空的(如果它不匹配)。辅助函数如下所示:

<T,U> Stream<U> toType(T t, Class<U> clazz) {
    if (clazz.isInstance(t)) {
        return Stream.of(clazz.cast(t));
    } else {
        return Stream.empty();
    }
}

(这大致基于某些 cmets 中提到的 C# 的 OfType 构造。)

在我们做的时候,让我们提取一个方法来创建一个ContextInfo

ContextInfo makeContextInfo(Context context) {
    // copy to another object -- not the important part
    final ContextInfo info = new ContextInfo(context.getPath());
    info.setThisPart(context.getThisPart());
    info.setNotImportant(context.getNotImportant());
    return info;
}

在这些提取之后,管道如下所示:

    return Arrays.stream(server.findServices())
        .map(service -> service.getContainer())
        .flatMap(container -> toType(container, Engine.class))
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .flatMap(possibleHost -> toType(possibleHost, Host.class))
        .flatMap(host -> Arrays.stream(host.findChildren()))
        .flatMap(possibleContext -> toType(possibleContext, Context.class))
        .map(this::makeContextInfo)
        .collect(Collectors.toList());

更好,我认为,我们已经删除了可怕的多行语句 lambda。

更新:奖励挑战

再一次,flatMap 是你的朋友。取流的尾部并将其迁移到尾部之前的最后一个flatMap。这样host 变量仍在作用域内,您可以将其传递给makeContextInfo 辅助方法,该方法已修改为也采用host

    return Arrays.stream(server.findServices())
        .map(service -> service.getContainer())
        .flatMap(container -> toType(container, Engine.class))
        .flatMap(engine -> Arrays.stream(engine.findChildren()))
        .flatMap(possibleHost -> toType(possibleHost, Host.class))
        .flatMap(host -> Arrays.stream(host.findChildren())
                               .flatMap(possibleContext -> toType(possibleContext, Context.class))
                               .map(ctx -> makeContextInfo(ctx, host)))
        .collect(Collectors.toList());

【讨论】:

  • 您可以仅凭此构建一个 API...了不起的工作。
  • 由于您正在为这里的应用程序设计toType,因此您可以将其重写为&lt;T,U&gt; Function&lt;T,Stream&lt;U&gt;&gt; toType(Class&lt;U&gt; clazz) { return t -&gt; { ... }; },以便仅使用.flatMap(toType(Engine.class)) 调用它,而无需创建另一个匿名 lambda 类。
【解决方案2】:

这将是我使用 JDK 8 流、方法引用和 lambda 表达式的代码版本:

server.findServices()
    .stream()
    .map(Service::getContainer)
    .filter(Engine.class::isInstance)
    .map(Engine.class::cast)
    .flatMap(engine -> Arrays.stream(engine.findChildren()))
    .filter(Host.class::isInstance)
    .map(Host.class::cast)
    .flatMap(host -> Arrays.stream(host.findChildren()))
    .filter(Context.class::isInstance)
    .map(Context.class::cast)
    .map(context -> {
        ContextInfo info = new ContextInfo(context.getPath());
        info.setThisPart(context.getThisPart());
        info.setNotImportant(context.getNotImportant());
        return info;
    })
    .collect(Collectors.toList());

在这种方法中,我将您的 if 语句替换为过滤谓词。考虑到instanceof 检查可以替换为Predicate&lt;T&gt;

Predicate<Object> isEngine = someObject -> someObject instanceof Engine;

也可以表示为

Predicate<Object> isEngine = Engine.class::isInstance

同样,您的演员表可以替换为Function&lt;T,R&gt;

Function<Object,Engine> castToEngine = someObject -> (Engine) someObject;

这几乎是一样的

Function<Object,Engine> castToEngine = Engine.class::cast;

在 for 循环中手动将项目添加到列表中可以替换为收集器。在生产代码中,将 Context 转换为 ContextInfo 的 lambda 可以(并且应该)提取到单独的方法中,并用作方法引用。

【讨论】:

  • 注意。吹。 . . .全部。超过。这。地板。
  • Class::isInstanceClass::cast 的方法引用的有趣用法。
  • 从 FP 的角度来看,变异的东西和做list.add() 感觉很古怪。从 OO 的角度来看,虽然这个示例显然比 OP 更易读,但它仍然远远落后于 LINQ,在 LINQ 中您将使用 OfType&lt;T&gt;() 而不是那个反射。
  • @HighCore 为了提高引用透明度,我们可以使用映射 lambda 来创建上下文信息,终端方法将是 collect(Collectors.toList()),然后可以使用它来对原始列表进行突变或只需更换它。不幸的是,Streams API 距离 LINQ 还差几年。也许再过 4 或 5 年 :-)
  • @HighCore:已修复。映射和使用收集器(正如我刚刚看到 Edwin Dalorzo 所建议的那样)是一种无副作用的方法。
【解决方案3】:

奖金挑战解决方案

受@EdwinDalorzo 回答的启发。

public List<ContextInfo> list() {
    final List<ContextInfo> list = new ArrayList<>();
    final StandardServer server = getServer();

    return server.findServices()
            .stream()
            .map(Service::getContainer)
            .filter(Engine.class::isInstance)
            .map(Engine.class::cast)
            .flatMap(engine -> Arrays.stream(engine.findChildren()))
            .filter(Host.class::isInstance)
            .map(Host.class::cast)
            .flatMap(host -> mapContainers(
                Arrays.stream(host.findChildren()), host.getName())
            )
            .collect(Collectors.toList());
}

private static Stream<ContextInfo> mapContainers(Stream<Container> containers,
    String hostname) {
    return containers
            .filter(Context.class::isInstance)
            .map(Context.class::cast)
            .map(context -> {
                ContextInfo info = new ContextInfo(context.getPath());
                info.setThisPart(context.getThisPart());
                info.setNotImportant(context.getNotImportant());
                info.setHostname(hostname); // The Bonus Challenge
                return info;
            });
}

【讨论】:

    【解决方案4】:

    第一次尝试超越丑陋。在我发现这本书可读之前,还需要几年的时间。必须是更好的方法。

    注意findChildren 方法返回的数组当然可以使用for (N n: array) 语法,但不能使用新的Iterable.forEach 方法。必须用Arrays.asList 包装它们

    public List<ContextInfo> list() {
        final List<ContextInfo> list = new ArrayList<ContextInfo>();
        final StandardServer server = getServer();
    
        asList(server.findServices()).forEach(service -> {
    
            if (!(service.getContainer() instanceof Engine)) return;
    
            final Engine engine = (Engine) service.getContainer();
    
            instanceOf(Host.class, asList(engine.findChildren())).forEach(host -> {
    
                instanceOf(Context.class, asList(host.findChildren())).forEach(context -> {
    
                    // copy to another object -- not the important part
                    final ContextInfo info = new ContextInfo(context.getPath());
                    info.setThisPart(context.getThisPart());
                    info.setNotImportant(context.getNotImportant());
                    list.add(info);
                });
            });
        });
    
        return list;
    }
    

    实用方法

    public static <T> Iterable<T> instanceOf(final Class<T> type, final Collection collection) {
        final Iterator iterator = collection.iterator();
        return () -> new SlambdaIterator<>(() -> {
            while (iterator.hasNext()) {
                final Object object = iterator.next();
                if (object != null && type.isAssignableFrom(object.getClass())) {
                    return (T) object;
                }
            }
            throw new NoSuchElementException();
        });
    }
    

    最后是Iterable 的支持 Lambda 的实现

    public static class SlambdaIterator<T> implements Iterator<T> {
        // Ya put your Lambdas in there
        public static interface Advancer<T> {
            T advance() throws NoSuchElementException;
        }
        private final Advancer<T> advancer;
        private T next;
    
        protected SlambdaIterator(final Advancer<T> advancer) {
            this.advancer = advancer;
        }
    
        @Override
        public boolean hasNext() {
            if (next != null) return true;
    
            try {
                next = advancer.advance();
    
                return next != null;
            } catch (final NoSuchElementException e) {
                return false;
            }
        }
    
        @Override
        public T next() {
            if (!hasNext()) throw new NoSuchElementException();
    
            final T v = next;
            next = null;
            return v;
        }
    
        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
    

    大量的管道和毫无疑问的 5 倍字节码。一定是更好的方法。

    【讨论】:

    • 我的第一个 lambda。离开这里是为了提醒大家不要做什么。
    • 这是一个有趣的例子,说明如果 Java 8 有 lambdas 但没有 Streams API 时可能需要编写的那种代码。
    • 我会接受的 :) 我认为这将是关于如何使用或滥用 Lambdas 的一个很好的参考。 “这就是你需要学习 Streams API 的原因”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-27
    • 1970-01-01
    • 2018-09-30
    • 2012-11-16
    • 2021-05-28
    • 2011-11-17
    相关资源
    最近更新 更多