问题 在Java 8中扩展List


我经常想将一个列表映射到另一个列表。例如,如果我有一个人的列表,我想要一个他们的名字列表,我想这样做:

目标 

List<Person> people = ... ;

List<String> names = people.map(x -> x.getName());

使用Java 8可以实现这样的功能:

JAVA 8版

List<String> names = people.stream()
                           .map(x -> x.getName())
                           .collect(Collectors.toList());

但这显然不是那么好。事实上,我认为使用番石榴更清洁:

GUAVA版本

List<String> names = Lists.transform(people, x -> x.getName());

但是,我喜欢链接。那么,我的目标可能吗?

我听说有人说Java 8默认方法类似于C#扩展方法。使用C#扩展方法,我可以轻松地添加一个辅助方法 IEnumerable<T>

public static IEnumerable<TRet> Map<T, TRet>(this IEnumerable<T> list, Func<T, TRet> selector)
{
    return list.Select(selector);
}

但是我无法弄清楚如何使用默认方法 扩展现有接口

此外,这显然是一个微不足道的例子。总的来说,我希望能够扩展 List<T> 和 Iterable<T> 接口使得与流api的交互更容易。


10402
2018-03-10 18:53


起源

注意 collect(Collectors.toList()) 将返回一份副本,而 Lists.transform 返回一个视图。 - Paul Bellora
您始终可以创建自己的界面并创建看起来像的代码 transform(list).using(x -> x.getName()) - fge
仅供参考:代替 x -> x.getName(),它使用起来更简单 Foo::getName。 - Louis Wasserman
@LouisWasserman我能做到的。我现在仍在决定是否更喜欢lambda语法......截至目前我认为开发人员发现阅读lambdas比方法引用更容易,特别是当来自coffeescript或C#或其他什么时。 - Alden
@Alden,Java类型系统不像Scala那样富有表现力,而Java不是动态语言,所以不可能(通过语言和库作者)改进API来做你想做的事情而不用失去灵活性(例如简单的并行性和懒惰)或添加 巨大 膨胀到现有的接口(例如,他们要添加 map 方法直接到 List 类)。而且由于Java没有提供任何扩展现有类型的方法,实际上你已经陷入困境。对不起:( - Vladimir Matveev


答案:


没有;你不能这样做。

默认方法与扩展方法不同;它们只能在原始界面中定义。


11
2018-03-10 18:56



那很不幸......我一直试图弄清楚这是否可能有一段时间。我认为你的声誉很大,你是对的:)。有什么建议使流api更干净? - Alden
@Alden,遗憾的是,Java没有解决方案 表达问题,与Scala和其他语言不同。所以你必须创建自己的类似DSL的包装器(或者只是静态函数,比如Guava)或者使用已经存在的东西。 - Vladimir Matveev
或者,您始终可以创建新的集合对象,在构造函数中使用list作为支持类型,并在新集合对象中创建所有功能。 - Damon Drake


如果你想要一个轻量级的视图 List 申请一个 Function 并支持链接你可以这样做:

import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;

public class MappingList<E> extends AbstractList<E> {
  // using this helper class we avoid carrying <S> with the public API
  static final class Source<E,S> {
      final List<S> list;
      final Function<? super S, ? extends E> mapper;
      Source(List<S> l, Function<? super S, ? extends E> m) {
          list=l;
          mapper=m;
      }
      E get(int index) { return mapper.apply(list.get(index)); }
      <T> Source map(Function<? super E, ? extends T> f) {
          Objects.requireNonNull(f);
          return new Source<>(list, mapper.andThen(f));
      }
      Stream<E> stream() { return list.stream().map(mapper); }
      Stream<E> parallelStream() { return list.parallelStream().map(mapper); }
    }
    final Source<E,?> source;

    private MappingList(Source<E,?> s) {
        Objects.requireNonNull(s);
        source=s;
    }
    @Override
    public E get(int index) {
        return source.get(index);
    }
    @Override
    public int size() {
        return source.list.size();
    }
    @Override
    public Stream<E> stream() {
        return source.stream();
    }
    @Override
    public Stream<E> parallelStream() {
        return source.parallelStream();
    }
    public <T> MappingList<T> map(Function<? super E, ? extends T> f) {
        return new MappingList<>(source.map(f));
    }
    public static <S,T> MappingList<T> map(
      List<S> l, Function<? super S, ? extends T> f) {
        Objects.requireNonNull(l);
        if(l instanceof MappingList)
            return ((MappingList<S>)l).map(f);
        return new MappingList<>(new Source<>(l, f));
    }
}

它支持GUAVA样式创建映射列表,同时仍允许使用 Stream 带有映射列表的API懒惰地评估所有值:

public static void main(String[] arg) {
    List<String> strings=Arrays.asList("a", "simple", "list");
    List<Integer> ints=MappingList.map(strings, s->compute(s));
    List<Integer> results=MappingList.map(ints, i->compute(i));
    for(int result:results) {
        System.out.println("first result: "+result);
        System.out.println("Not computing any more values");
        break;
    }
    System.out.println();
    System.out.println("  interacting with stream API:");
    System.out.println(results.stream().filter(i-> i>500).findFirst());
}
public static int compute(String s) {
    System.out.println("doing computation for "+s);
    return Integer.parseInt(s, 36);
}
public static int compute(int i) {
    System.out.println("doing computation for "+i);
    return i*i;
}
做计算
做计算10
第一个结果:100
不再计算任何值

  与流API交互:
做计算
做计算10
做简单的计算
为1724345618做计算
可选[410277188]

如果你想创建一个 List 使用预先计算的值,您可以简单地使用 new ArrayList<>(mappedList)


2
2018-03-11 17:50





吃我自己的狗的食物并实施我在评论中建议的内容(UNTESTED,但它应该有用 - 请注意你应该使用 super 在适当的情况下,这不是我的强项):

public final class ListTransformer<T>
{
    private final List<T> inputList;

    public static <X> ListTransformer<X> transform(final List<X> inputList)
    {
        return new ListTransformer<X>(inputList);
    }

    private ListTransformer(final List<T> inputList)
    {
        this.inputList = inputList;
    }

    public <U> List<U> using(final Function<T, U> f)
    {
        return inputList.stream().map(f).collect(Collectors.toList());
    }
}

用法:

import static wherever.is.ListTransformer.transform;

//
final List<String> names = transform(personList).using(x -> x.getName());

1
2018-03-10 19:15



我相信你的意思 import static wherever.is.ListTransformer.transform 在你的例子:) - Vladimir Matveev
@VladimirMatveev呃,是的,脑屁 - fge
@Holger为什么不呢。但另一方面,为什么不呢?有些人喜欢流畅的界面...... - fge
@Holger,在我看来是 你的 没有任何好处的解决方案;最终这是一个品味问题。 OP喜欢链接,我的解决方案提供链接,您的解决方案提供链接,就是这样 - fge


C#扩展方法非常棒,并且提供了使用自己的方法扩展C#集合的简单机制。

但现在java中有流。 基于 @fge 响应我带着这段代码在流上编写自己的扩展方法:

public final class StreamExtender<T>
{
    private final Stream<T> _inputStream;

    public static <T> StreamExtender<T> extend(final Stream<T> inputStream)
    {
        return new StreamExtender<>(inputStream);
    }

    private StreamExtender(final Stream<T> inputStream)
    {
        this._inputStream = inputStream;
    }

    public <U> List<U> extensionMethod(final Function<T, U> f)
    {
        // your own code.
        return _inputStream.map(f).collect(Collectors.toList());
    }
}

并看到它在行动:

Integer[] array = { 1, 2, 3};
List<String> result = StreamExtender.extend(stream(array)).extensionMethod(Object::toString);


1
2018-06-27 09:24