[FIXED] why a function like this idGetter can not be applied to Map function in java stream api?

Issue

public <T> List<Long> leafIds(Map<Long, ? extends List<? extends T>> childrenMap, Long parentId, Function<? extends T, Long> idGetter) {
    if (!childrenMap.containsKey(parentId)) {
        return Collections.singletonList(parentId);
    }
    List<Long> result = new ArrayList<>();
    List<Long> childrenIds = new ArrayList<>();
    childrenIds.add(parentId);
    for (int i = 0; i < childrenIds.size(); i++) {
        Long childId = childrenIds.get(i);
        List<? extends T> children = childrenMap.get(childId);
        if (CollectionUtils.isNotEmpty(children)) {
            childrenIds.addAll(children.stream().filter(Objects::nonNull).map(idGetter).collect(Collectors.toList()));
            continue;
        }
        result.add(childId);

    }
    return result;
}

error mark
there is an error mark under line 12, the editor indicates that type of [idGetter] is wrong,how can I fix this?
I’ll be grateful for any help!

Solution

Function<? extends T, Long> means:

Any function that converts some specific but unknown thing to a Long, where we do know that, whatever it is, it’s either T or some subtype thereof.

It specifically does not mean: "A function that can take any T and convert it to a Long". If you want that, you’d need a Function<T, Long>.

List<? extends T> means:

A list whose elements are constrained; they must all be instanceof some type we don’t know. We do know that, whatever type it is, it is at least T or some subtype of T.

It specifically does not mean: A list whose elements are constrained to be instanceof T. Because that’d just be a List<T>.

The expression list.get(0), if list is a List<T>, is itself T. Obviously. It’s still T if the list is a List<? extends T> instead. The crucial difference is with add: you can call list.add(someInstanceOfT) just fine, if the type of list is List<T>. However, list.add(anything) doesn’t compile if list‘s type is List<? extends T>.

Think about it:

List<Integer> myList = new ArrayList<Integer>();
List<? extends Number> numbers = myList;
numbers.add(someDouble);

The above does not compile – specifically, the third line (numbers.add) does not. And now it should be obvious why: The numbers variable is pointing at a list whose constraint is unknown. Maybe it is Integer, which would mean adding a double to it, is broken.

The key thing to keep in mind is PECS: Produce-Extends-Consume-Super. This is from the point of view of the object: a list.get() call "produces" a value, therefore, extends bounds are useful (a List<? extends Number> can be used for meaningful production, because PE: Produce-Extends: .get() call, which produces, gives you Number. Or flip it around, CS: a List<? super Number> can meaningfully consume: list.add(someNumber) is allowed on them. But list.get(0) on a List<? super Number> is not particularly useful (it gets you an object, because all lists can’t store anything else, but nothing more specific).

Now back to functions. The first typearg of a function is solely used for consumption: The point of that first arg is that your function consumes these. The second argument is solely used for production.

Thinking about PECS, Function<? extends Anything, _> is completely useless, given that extends means its useful only for production, and the first typearg of Function is solely used for consumption.

Java is ‘correct’, your code is broken. What did you really mean?

Two options:

Option 1

If you intend to accept a function that can process any T, then it should be either a Function<T or a Function<? super T, as per PECS rules.

Option 2

Generics serve to link things, given that they are a figment of the compiler’s imagination (java the VM doesn’t know what they are, and most of the type args are erased entirely by javac). Perhaps you intended to link the type used in the first parameter with the type used in that function. Then.. make a type variable for that purpose, that is their job. You’ve already done that, so, fix up your bounds so they align with PECS:

public <T> List<Long> leafIds(Map<Long, ? extends List<? extends T>> childrenMap, Long parentId, Function<? super T, Long> idGetter) {

All I did was swap ? extends to ? super. What that says is:

There is some specific type. No idea what it is, though. Let’s call it T.

The first argument gives us a map that maps to a list of Xs, where X is either that unknown type T or some subtype of that.

The third argument is a mapping function that is capable of converting some unknown type to something else. The unknown type it can convert is, however, guaranteed to be either T or some supertype of it.

That paints a complete, type-wise consistent picture. Let’s say the map gives out Integers and the function can convert any Object (so, T is Number, the list are a subtype of T: Integer, check, and the mapping function can convert any Object, that’s a supertype of T, so, check). That works out.

Now let’s go in reverse:

The lists have Numbers in them, and the mapping function can only convert Doubles.

So what happens if one of the elements in the list is an Integer instead? Type safety broken.

Answered By – rzwitserloot

Answer Checked By – Gilberto Lyons (Easybugfix Admin)

Leave a Reply

(*) Required, Your email will not be published