Javagenerics – 实现像map这样的高阶函数
我决定在Java中编写一些常见的高阶函数(map,filter,reduce等),这些函数通过generics是类型安全的,而且我遇到了在一个特定函数中匹配通配符的问题。
为了完整,函子接口是这样的:
/** * The interface containing the method used to map a sequence into another. * @param The type of the elements in the source sequence. * @param The type of the elements in the destination sequence. */ public interface Transformation { /** * The method that will be used in map. * @param sourceObject An element from the source sequence. * @return The element in the destination sequence. */ public R apply(S sourceObject); }
令人不安的function就像一个地图 ,但不是转换一个集合,而是转换一个Map (起初我认为它应该被称为mapMap
,但它听起来很愚蠢,我最终称之为remapEntries
)。
我的第一个版本是(并且坐下来,因为签名是一个非常怪物):
/** * * Fills a map with the results of applying a mapping function to * a source map. *
* Considerations: * * - The result map must be non-null, and it's the same object what is returned * (to allow passing an unnamed new Map as argument).
* - If the result map already contained some elements, those won't * be cleared first.
* - If various elements have the same key, only the last entry given the * source iteration order will be present in the resulting map (it will * overwrite the previous ones).
*
* * @param Type of the source keys. * @param Type of the source values. * @param Type of the result keys. * @param Type of the result values. * @param * @param f The object that will be used to remapEntries. * @param source The map with the source entries. * @param result The map where the resulting entries will be put. * @return the result map, containing the transformed entries. */ public static <SK, SV, RK, RV, MapRes extends Map> MapRes remapEntries(final Transformation<Map.Entry, Map.Entry> f, final Map source, MapRes result) { for (Map.Entry entry : source.entrySet()) { Map.Entry res = f.apply(entry); result.put(res.getKey(), res.getValue()); } return result; }
它似乎是非常正确的,但问题是所使用的转换必须完全匹配类型参数,这使得难以重用兼容类型的map函数。 所以我决定在签名中添加通配符,结果如下:
public static <SK, SV, RK, RV, MapRes extends Map> MapRes remapEntries(final Transformation<? super Map.Entry, ? extends Map.Entry> f, final Map source, MapRes result) { for (Map.Entry entry : source.entrySet()) { Map.Entry res = f.apply(entry); result.put(res.getKey(), res.getValue()); } return result; }
但是当我试图测试它时,通配符匹配失败:
@Test public void testRemapEntries() { Map things = new HashMap(); things.put("1", 1); things.put("2", 2); things.put("3", 3); Transformation<Map.Entry, Map.Entry> swap = new Transformation<Entry, Entry>() { public Entry apply(Entry sourceObject) { return new Pair(sourceObject.getValue().intValue(), sourceObject.getKey()); //this is just a default implementation of a Map.Entry } }; Map expected = new HashMap(); expected.put(1, "1"); expected.put(2, "2"); expected.put(3, "3"); Map result = IterUtil.remapEntries(swap, things, new HashMap()); assertEquals(expected, result); }
错误是:
method remapEntries in class IterUtil cannot be applied to given types required: Transformation<? super java.util.Map.Entry,? extends java.util.Map.Entry>,java.util.Map,MapRes found: Transformation<java.util.Map.Entry,java.util.Map.Entry>,java.util.Map,java.util.HashMap
那么,有关如何解决此问题的任何提示? 或者我应该放弃并为此编写显式循环? ^ _ ^
我想你应该看看Google Guava API 。
在那里你可以找到一个与你的转换类似的function界面。 还有一个带有实用程序方法的类Maps来创建或转换地图实例。
在实施仿制药使用方法时,您还应该考虑PECS 。
这是一个困难的问题。 以下知识完全无用,没有人应该关心:
首先要解决的是swap
类型。 输入类型不应该是Entry
,因为它不能接受Entry
,它不是E
的子类型。 但是, E
是E extends S,? extends N>
的子类型E extends S,? extends N>
E extends S,? extends N>
E extends S,? extends N>
。 所以我们的变压器应该把它作为输入。 对于输出,没有外卡,因为变压器无论如何都只能实例化一个具体类型。 我们只想诚实准确地了解可以消费什么以及将要生产什么:
/* */ Transformation< Entry extends String, ? extends Number>, Entry > swap = new Transformation< Entry extends String, ? extends Number>, Entry> () { public Entry apply( Entry extends String, ? extends Number> sourceObject) { return new Pair( sourceObject.getValue().intValue(), sourceObject.getKey() ); } };
注意String
是最终的,没有人扩展它,但我担心通用系统不是那么聪明,所以原理问题我做了? extends String
无论如何, ? extends String
,以便以后使用。
然后,让我们考虑一下remapEntries()
。 我们怀疑大多数变形金刚通过它会有与swap
类似的声明,因为我们提出了合理的理由。 所以我们最好
remapEntry( Transformation< Entry extends SK, ? extends SV>, Entry > f, ...
正确匹配该参数。 从那里,我们计算出源和结果的类型,我们希望它们尽可能通用:
public static > RM remapEntries( Transformation< Entry extends SK, ? extends SV>, Entry > f, Map extends SK, ? extends SV> source, RM result ) { for(Entry extends SK, ? extends SV> entry : source.entrySet()) { Entry res = f.apply(entry); result.put(res.getKey(), res.getValue()); } return result; }
RM
没有必要,可以直接使用Map super RK, ? super RV>
Map super RK, ? super RV>
Map super RK, ? super RV>
。 但似乎您希望返回类型与调用者上下文中的result
类型相同。 我只是简单地使返回类型void
– 已经有足够的麻烦了。
如果swap
不使用,这件事会失败? extends
? extends
。 例如,如果输入类型是String-Integer
,那么这很荒谬? extends
? extends
他们。 但是你可以使用不同参数类型声明的重载方法来匹配这种情况。
好吧,出于好运,这很有效。 但是,这完全不值得。 如果你忘了它,你的生活会好得多,并且使用原始类型,用英语记录参数,在运行时进行类型检查。 问问自己,通用版本能为您买到什么吗? 很少,以极高的价格渲染您的代码完全不可理解。 如果我们明天早上阅读方法签名,那么包括你自己和我自己在内的任何人都无法理解它。 它比正则表达更糟糕。
突然出现了一些东西:如果嵌套通用参数中的通配符不会被捕获,因为它们实际上是类型的一部分,那么我可以在映射中使用反向边界而不是在Transformation
中使用它们。
public static > MapRes remapEntries(final Transformation, Map.Entry> f, final Map extends SK, ? extends SV> source, MapRes result) { for (Map.Entry extends SK, ? extends SV> entry : source.entrySet()) { Map.Entry extends RK, ? extends RV> res = f.apply((Map.Entry)entry); result.put(res.getKey(), res.getValue()); } return result; }
唯一的问题是我们必须在Transformation.apply
进行未经检查的强制Transformation.apply
。 如果Map.Entry
接口是只读的 ,那将是完全安全的,所以我们可以交叉指向并希望转换不会尝试调用Map.Entry.setValue
。
如果调用setValue
方法以确保至少运行时类型安全,我们仍然可以传递Map.Entry
接口的不可变包装器,该接口引发exception。
或者只是创建一个显式的不可变Entry接口并使用它,但这有点像作弊(因为有两个不同的转换):
public interface ImmutableEntry { public K getKey(); public V getValue(); } public static > RM remapEntries(final Transformation, Map.Entry> f, final Map extends SK, ? extends SV> source, RM result) { for (final Map.Entry extends SK, ? extends SV> entry : source.entrySet()) { Map.Entry extends RK, ? extends RV> res = f.apply(new ImmutableEntry() { public SK getKey() {return entry.getKey();} public SV getValue() {return entry.getValue();} }); result.put(res.getKey(), res.getValue()); } return result; }