Java generics - implementing higher order functions like map

只谈情不闲聊 提交于 2019-12-03 13:44:40
hhbarriuso

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.

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 be Entry<String,Number>, because then it cannot accept Entry<String,Integer>, which is not a subtype of E<S,N>. However, E<S,I> is a subtype of E<? 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:

    /*     */ Transformation<
                  Entry<? extends String, ? extends Number>, 
                  Entry<Integer, String>
              > swap
        = new Transformation<
                  Entry<? extends String, ? extends Number>, 
                  Entry<Integer, String>> () 
    {
        public Entry<Integer, String> apply(
            Entry<? extends String, ? extends Number> sourceObject) 
        {
            return new Pair<Integer, String>(
                sourceObject.getValue().intValue(), 
                sourceObject.getKey()
            );
        }
    };

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 the swap, because of the justifications we laid out. So we better have

remapEntry( 
    Transformation<
        Entry<? extends SK, ? extends SV>,
        Entry<RK,RV>
        > f,
    ...

to properly match that argument. From there, we work out the type of source and result, we want them to be as general as possible:

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>>
RM remapEntries(
    Transformation<
        Entry<? extends SK, ? extends SV>,
        Entry<RK,RV>
        > f,
    Map<? extends SK, ? extends SV> source,
    RM result
)
{
    for(Entry<? extends SK, ? extends SV> entry : source.entrySet()) {
        Entry<RK,RV> res = f.apply(entry);
        result.put(res.getKey(), res.getValue());
    }
    return result;
}

RM isn't necessary, it's fine to use directly Map<? super RK, ? super RV>. But it seems that you want the return type identical to the result type in caller's context. I woulda simply made the return type void - there is enough trouble already.

This thing will fail, if swap does not use ? extends. For example if the input type is String-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.

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.

public static <SK, SV, RK, RV, MapRes extends Map<? super RK, ? super RV>>
  MapRes remapEntries(final Transformation<Map.Entry<SK, SV>,
                                           Map.Entry<RK, RV>> 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<SK, SV>)entry);
        result.put(res.getKey(), res.getValue());
    }
    return result;
}

The only problem is that we have to do the unchecked cast in the Transformation.apply. It would be totally safe if the Map.Entry interface were read-only, so we can just cross fingers and hope that the transformation does not try to call Map.Entry.setValue.

We could still pass an immutable wrapper of the Map.Entry interface that threw an exception if the setValue 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):

public interface ImmutableEntry<K, V> {
    public K getKey();
    public V getValue();
}

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>> RM remapEntries(final Transformation<ImmutableEntry<SK, SV>, Map.Entry<RK, RV>> 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<SK, SV>() {
            public SK getKey() {return entry.getKey();}
            public SV getValue() {return entry.getValue();}
        });
        result.put(res.getKey(), res.getValue());
    }
    return result;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!