javalistgenericscollectionsjava-stream

JwkSet gives Cannot resolve constructor 'JWKSet(List<RSAKey>)' - Stream.collect(Collectors.toList()) vs Stream.toList()


In our code, we use JWKset from the nimbus-jose-jwt library in a method that creates a map that we use for stuff. As you can see in the return, we use a stream that calls map on a list of RSAkey, and then we create a List from that, which is used to create a JWKSet. We use collect(Collections.toList()) and that works, but when I tried to change this to use toList() which our SonarQube suggested we should, it does not work.

The compiler error is:

Cannot resolve constructor 'JWKSet(List)

I also get the suggestion to 'cast argument to JWK'. But RSAKey is a class that extends the abstract class JWK if you look at the documentation https://github.com/felx/nimbus-jose-jwt/blob/master/src/main/java/com/nimbusds/jose/jwk/RSAKey.java

I do not see how the two method calls, which create the same type of output, do not work the same here.

final List<RSAKey> keys = Collections.singletonList(identityToRSAKey(certificate));
    
// Evidence the methold calls create the same type of output
List<RSAKey> list = keys.stream().map(RSAKey::toPublicJWK).toList();
List<RSAKey> collect = keys.stream().map(RSAKey::toPublicJWK).collect(Collectors.toList());

//This works
JWKSet mySet = new JWKSet(keys
        .stream()
        .map(RSAKey::toPublicJWK)
        .collect(Collectors.toList()));

//This does not work
JWKSet mySet2 = new JWKSet(keys
        .stream()
        .map(RSAKey::toPublicJWK)
        .toList() );

// Where we actually use the code
return new JWKSet(keys
    .stream()
    .map(RSAKey::toPublicJWK)
    .collect(Collectors.toList() ))
    .toPublicJWKSet()
.toJSONObject();

Constructor in JWKSet that we try to invoke

public JWKSet(List<JWK> keys) {
    this(keys, Collections.emptyMap());
}

Solution

  • The compiler error Cannot resolve constructor 'JWKSet(List<RSAKey>)' is caused by the different signatures of collect() and toList().

    <R,A> R collect(Collector<? super T,A,R> collector)
    
    List<T> toList()
    

    The terminal operation collect() accepts a Collector, which is simply a mutable reduction operation, that starts from a type T (in your case, RSAKey), and returns a container R of accumulated transformed results.

    accumulates input elements into a mutable result container, optionally transforming the accumulated result into a final representation after all input elements have been processed

    When you assign the result of the stream to List<RSAKey> collect, no transformation is performed on the RSAKey elements. The end result R basically corresponds to a list of the supplied elements (List<T>, in your case List<RSAKey>). However, when the same stream appears as an argument for JWKSet's constructor, the compiler tries to find a matching signature, and the only one available is JWKSet(List<JWK>). Therefore, every RSAKey element of the stream is transformed (upcast) to JWK, returning List<JWK>.

    On the other hand, toList() simply returns a List of the elements streamed. Therefore, the list you're obtaining is List<RSAKey>, and not List<JWK>. This is because toPublicJWK() return type is RSAKey (not JWK), and since List<RSAKey> is not a subtype of List<JWK> (even though RSAKey is a subtype of JWK), you're receiving a compiler error when providing the stream to JWKSet's constructor. For more details about subtyping with generics, I recommend this article by Angelika Langer.

    In your case, I would just stick with collect(), but if you really want (or need) to use toList(), you can simply cast each element of the stream to JWK.

    JWKSet mySet = new JWKSet(keys
        .stream()
        .map(RSAKey::toPublicJWK)   // I've left this invocation, because I don't know if it's really necessary to what you're doing
        .map(JWK.class::cast)
        .toList());