Java泛型 - 实现像map这样的高阶函数
我决定用 Java 编写一些常见的高阶函数(map、filter、reduce 等),这些函数通过泛型实现类型安全,但我在一个特定函数中遇到通配符匹配问题。
为了完整起见,函子接口是这样的:
/**
* The interface containing the method used to map a sequence into another.
* @param <S> The type of the elements in the source sequence.
* @param <R> The type of the elements in the destination sequence.
*/
public interface Transformation<S, R> {
/**
* 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);
}
令人不安的函数就像一个map,但不是转换Collection,而是转换Map (起初我认为它应该被称为 mapMap
,但它听起来很愚蠢,所以我最终将其称为 remapEntries
)。
我的第一个版本是(请坐一下,因为签名相当怪物):
/**
* <p>
* Fills a map with the results of applying a mapping function to
* a source map.
* </p>
* Considerations:
* <ul>
* <li>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).</li>
* <li>If the result map already contained some elements, those won't
* be cleared first.</li>
* <li>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).</li>
* </ul>
*
* @param <SK> Type of the source keys.
* @param <SV> Type of the source values.
* @param <RK> Type of the result keys.
* @param <RV> Type of the result values.
* @param <MapRes>
* @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<RK, RV>> MapRes remapEntries(final Transformation<Map.Entry<SK, SV>, Map.Entry<RK,RV>> f, final Map<SK, SV> source, MapRes result) {
for (Map.Entry<SK, SV> entry : source.entrySet()) {
Map.Entry<RK, RV> res = f.apply(entry);
result.put(res.getKey(), res.getValue());
}
return result;
}
它似乎非常正确,但问题是使用的转换必须与类型参数完全匹配,这使得很难重用类型的映射函数是兼容的。所以我决定在签名中添加通配符,结果如下:
public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<? super Map.Entry<? super SK, ? super SV>, ? extends Map.Entry<? extends RK, ? extends RV>> f, final Map<SK, SV> source, MapRes result) {
for (Map.Entry<SK, SV> entry : source.entrySet()) {
Map.Entry<? extends RK, ? extends RV> res = f.apply(entry);
result.put(res.getKey(), res.getValue());
}
return result;
}
但是当我尝试测试它时,通配符匹配失败:
@Test
public void testRemapEntries() {
Map<String, Integer> things = new HashMap<String, Integer>();
things.put("1", 1);
things.put("2", 2);
things.put("3", 3);
Transformation<Map.Entry<String, Number>, Map.Entry<Integer, String>> swap = new Transformation<Entry<String, Number>, Entry<Integer, String>>() {
public Entry<Integer, String> apply(Entry<String, Number> sourceObject) {
return new Pair<Integer, String>(sourceObject.getValue().intValue(), sourceObject.getKey()); //this is just a default implementation of a Map.Entry
}
};
Map<Integer, String> expected = new HashMap<Integer, String>();
expected.put(1, "1");
expected.put(2, "2");
expected.put(3, "3");
Map<Integer, String> result = IterUtil.remapEntries(swap, things, new HashMap<Integer, String>());
assertEquals(expected, result);
}
错误是:
method remapEntries in class IterUtil cannot be applied to given types
required: Transformation<? super java.util.Map.Entry<? super SK,? super SV>,? extends java.util.Map.Entry<? extends RK,? extends RV>>,java.util.Map<SK,SV>,MapRes
found: Transformation<java.util.Map.Entry<java.lang.String,java.lang.Number>,java.util.Map.Entry<java.lang.Integer,java.lang.String>>,java.util.Map<java.lang.String,java.lang.Integer>,java.util.HashMap<java.lang.Integer,java.lang.String>
那么,有关如何解决此问题的任何提示吗?或者我应该放弃并为此编写显式循环? ^_^
I decided to write some common Higher Order Functions in Java (map, filter, reduce, etc.) that are type safe via generics, and I'm having problems with wildcards matching in one particular function.
Just to be complete, the functor interface is this:
/**
* The interface containing the method used to map a sequence into another.
* @param <S> The type of the elements in the source sequence.
* @param <R> The type of the elements in the destination sequence.
*/
public interface Transformation<S, R> {
/**
* 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);
}
The troubling function is like a map, but instead of transforming a Collection it transforms a Map (at first I thought it should be called mapMap
, but it sounded so stupid that I ended up calling it remapEntries
).
My first version was (and take a sit, because the signature is quite a monster):
/**
* <p>
* Fills a map with the results of applying a mapping function to
* a source map.
* </p>
* Considerations:
* <ul>
* <li>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).</li>
* <li>If the result map already contained some elements, those won't
* be cleared first.</li>
* <li>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).</li>
* </ul>
*
* @param <SK> Type of the source keys.
* @param <SV> Type of the source values.
* @param <RK> Type of the result keys.
* @param <RV> Type of the result values.
* @param <MapRes>
* @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<RK, RV>> MapRes remapEntries(final Transformation<Map.Entry<SK, SV>, Map.Entry<RK,RV>> f, final Map<SK, SV> source, MapRes result) {
for (Map.Entry<SK, SV> entry : source.entrySet()) {
Map.Entry<RK, RV> res = f.apply(entry);
result.put(res.getKey(), res.getValue());
}
return result;
}
And it seems to be quite correct, but the problem is that the transformation used must match exactly the type parameters, making difficult to reuse map functions for types that are compatible. So I decided to add wildcards to the signature, and it ended up like this:
public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<? super Map.Entry<? super SK, ? super SV>, ? extends Map.Entry<? extends RK, ? extends RV>> f, final Map<SK, SV> source, MapRes result) {
for (Map.Entry<SK, SV> entry : source.entrySet()) {
Map.Entry<? extends RK, ? extends RV> res = f.apply(entry);
result.put(res.getKey(), res.getValue());
}
return result;
}
But when I'm trying to test it, wildcard matching fails:
@Test
public void testRemapEntries() {
Map<String, Integer> things = new HashMap<String, Integer>();
things.put("1", 1);
things.put("2", 2);
things.put("3", 3);
Transformation<Map.Entry<String, Number>, Map.Entry<Integer, String>> swap = new Transformation<Entry<String, Number>, Entry<Integer, String>>() {
public Entry<Integer, String> apply(Entry<String, Number> sourceObject) {
return new Pair<Integer, String>(sourceObject.getValue().intValue(), sourceObject.getKey()); //this is just a default implementation of a Map.Entry
}
};
Map<Integer, String> expected = new HashMap<Integer, String>();
expected.put(1, "1");
expected.put(2, "2");
expected.put(3, "3");
Map<Integer, String> result = IterUtil.remapEntries(swap, things, new HashMap<Integer, String>());
assertEquals(expected, result);
}
The error is:
method remapEntries in class IterUtil cannot be applied to given types
required: Transformation<? super java.util.Map.Entry<? super SK,? super SV>,? extends java.util.Map.Entry<? extends RK,? extends RV>>,java.util.Map<SK,SV>,MapRes
found: Transformation<java.util.Map.Entry<java.lang.String,java.lang.Number>,java.util.Map.Entry<java.lang.Integer,java.lang.String>>,java.util.Map<java.lang.String,java.lang.Integer>,java.util.HashMap<java.lang.Integer,java.lang.String>
So, any hints on how to fix this? Or should I give up and write explicit loops for this? ^_^
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我认为您应该查看 Google Guava API。
您可以在那里找到函数 界面类似于您的转换界面。还有一个地图 使用实用方法来创建或转换地图实例。
在实现泛型使用方法时,您还应该考虑 PECS。
I think you should take a look to Google Guava API.
There you can find a Function interface similar to your Transformation one. There is also a class Maps with utility methods to create or transform map instances.
You should also consider PECS when implementing methods for generics use.
这是一个困难的问题。以下知识完全无用,没有人应该关心拥有:
首先要修复的是
swap
的类型。输入类型不应该是Entry
,因为这样它就不能接受Entry
,它不是E
E
的子类型。 S,N>
。然而,E
是E
。所以我们的变压器应该将其作为输入。对于输出,没有通配符,因为变压器无论如何只能实例化一个具体类型。我们只是想诚实准确地了解可以消耗什么以及将产生什么:注意
String
是最终的,没有人扩展它,但我担心通用系统并没有那么聪明地知道那么,原则上,我做了?无论如何,都会扩展 String
,以便以后使用。然后,让我们考虑一下
remapEntries()
。由于我们提出的理由,我们怀疑传递给它的大多数转换器将具有与交换类似的类型声明。所以我们最好必须正确地匹配这个论点。从那里,我们计算出源和结果的类型,我们希望它们尽可能通用:
RM
不是必需的,直接使用Map
。但您似乎希望返回类型与调用者上下文中的result
类型相同。我只想将返回类型设置为 void - 已经有足够多的麻烦了。如果
swap
不使用,这个东西就会失败?扩展
。例如,如果输入类型是String-Integer
,那么执行就很荒谬?扩展了它们的
。但是您可以使用具有不同参数类型声明的重载方法来匹配这种情况。好吧,这成功了,纯粹是运气好。但是,这完全不值得。如果你忘记它,并使用原始类型,用英语记录参数,在运行时进行类型检查,你的生活会好得多。问问自己,通用版本能给你带来什么吗?很少,但代价却是让你的代码完全无法理解。如果我们明天早上阅读方法签名,没有人,包括你自己和我自己,能够理解它。它比正则表达式糟糕得多。
This is a difficult one. The following knowledge is totally useless and nobody should care to posses:
First thing to fix is the type of
swap
. The input type should not beEntry<String,Number>
, because then it cannot acceptEntry<String,Integer>
, which is not a subtype ofE<S,N>
. However,E<S,I>
is a subtype ofE<? extends S,? extends N>
. So our transformer should take that as input. For the output, no wild card, because the transformer can only instantiate a concrete type anyway. We just want to be honest and accurate of what can be consumed and what will be produced:Note
String
is final and nobody extends it, but I'm afraid the generic system isn't that smart to know that, so as a matter of principle, I did? extends String
anyway, for later good.Then, let's think about
remapEntries()
. We suspect that most transformers pass to it will have similar type declaration as theswap
, because of the justifications we laid out. So we better haveto properly match that argument. From there, we work out the type of source and result, we want them to be as general as possible:
RM
isn't necessary, it's fine to use directlyMap<? super RK, ? super RV>
. But it seems that you want the return type identical to theresult
type in caller's context. I woulda simply made the return typevoid
- there is enough trouble already.This thing will fail, if
swap
does not use? extends
. For example if the input type isString-Integer
, it's ridiculous to do? extends
of them. But you can have a overloading method with different parameter type declaration to match this case.Ok, that worked, out of sheer luck. But, it is totally not worth it. Your life is much better if you just forget about it, and use raw type, document the parameters in English, do type check at runtime. Ask yourself, does the generic version buy you anything? Very little, at the huge price of rendering your code completely incomprehensible. Nobody, including yourself, and myself, could make sense of it if we read the method signature tomorrow morning. It is much much worse than regex.
我突然想到了一些事情:如果嵌套泛型参数中的通配符不会被捕获,因为它们实际上是类型的一部分,那么我可以在映射中使用反向边界,而不是在
Transformation 中使用它们
。唯一的问题是我们必须在 Transformation.apply 中进行未经检查的转换。如果 Map.Entry 接口是只读的,那将是完全安全的,因此我们可以祈祷并希望转换不会尝试调用 Map .Entry.setValue。
我们仍然可以传递
Map.Entry
接口的不可变包装器,如果调用setValue
方法,该包装器会抛出异常,以确保至少运行时类型安全。或者只是创建一个显式的不可变 Entry 接口并使用它,但这有点像作弊(因为有两个不同的转换):
Something has just suddenly popped into my head: if the wildcards in nested generic parameters won't be captured as they are literally part of the type, then I could use the reverse bounds in the maps instead of using them in the
Transformation
.The only problem is that we have to do the unchecked cast in the
Transformation.apply
. It would be totally safe if theMap.Entry
interface were read-only, so we can just cross fingers and hope that the transformation does not try to callMap.Entry.setValue
.We could still pass an immutable wrapper of the
Map.Entry
interface that threw an exception if thesetValue
method was called to ensure at least runtime type safety.Or just make an explicit immutable Entry interface and use it, but that's a little bit like cheating (as having two different Transformations):