3

The following test renders fine in my IDE (Eclipse), but fails to compile when building via Maven.

The compiler error is shown in the comment line in the code block below.

It looks like the compiler is unable to determine the type of the 'o' input to the lambda. And if I cast o to the MyObj class, then it compiles fine.

I realize that this is a somewhat convoluted situation (we really do need this complexiy, though). And there really should be enough type info here for the compiler to determine the type (and the built-in compiler in Eclipse does so).

Am I doing something wrong with the generics declarations?

JDK is 21.0.5

public class AnotherTestClass {

    public static class MyObj{
        private final String arg1;
        
        public MyObj(String arg1) {
            this.arg1 = arg1;
        }
        
        public String getArg1() { return arg1; }
    }

    public static class MyFunctionHolder<T, R>{
        Function<T, R> f;

        public MyFunctionHolder(Function<T, R> f) {
            this.f = f;
        }
        
    }
    
    public static <C extends Collection<E>, E, R> MyFunctionHolder<C, R> forCollectionOfType(Class<? extends C> collectionClass, Class<E> elementClass, Function<C, R> function){
        return new MyFunctionHolder<C, R>(function);
    }

    
    @Test
    public void testMultiLevelStreams() {
        List<MyObj> list = Arrays.asList(new MyObj("one"), new MyObj("two"));
        
        MyFunctionHolder<List<MyObj>, List<String>> fh = forCollectionOfType(   List.class, 
                                                    MyObj.class, 
                                                    l -> l.stream()
                                                    .map(o -> o.getArg1())  // compile error "cannot find symbol\n  symbol:   method getArg1()\n  variable o of type java.lang.Object"
                                                    .toList()
                            );
        
        List<String> rslt = fh.f.apply(list);
        
    }

}

The objective is to have the static forCollectionOfType method accept the class of a collection, the class of the elements in the collection, and a function to apply to the collection itself.

One potentially useful data point is that when I do this, I get compile errors in Eclipse as well - so I'm really thinking I must be doing something wrong with the generics:

    Function<List<MyObj>, List<String>> listFunction = l -> l.stream().map(o -> o.getArg1()).toList();
    
    MyFunctionHolder<List<MyObj>, List<String>> fh2 = forCollectionOfType(  List.class, 
            MyObj.class, 
            listFunction
            );

Update

After reading this: Difference between <? super T> and <? extends T> in Java

I wound up making a small change to the generics in the method declaration:

Instead of Class<? extends C> collectionClass, I changed it to Class<? super C> collectionClass

The compiler error is now gone. However, I can now pass Object.class to the static method - and there is no compile time check to make sure the function generic parameters are consistent with the collection and element classes we pass in:

    MyFunctionHolder<List<MyObj>, List<String>> fh = forCollectionOfType(   Object.class, // Whoa - that's not a Collection!
                                                MyObj.class, 
                                                l -> l.stream()
                                                .map(o -> o.getArg1())
                                                .toList()

It actually seems like I could remove the collection type entirely (the generic type checking is going to come from the function type).

However, we have downstream reasons for needing the collection class - but it looks like there is no way to ensure that the collection type is consistent with the function.

Let me know if I'm wrong or missing something!

5
  • Your function type indicates it will take a C (here a List<MyObj>) and return an R (here, a String). But it looks like you're attempting to return a List<String> in your lambda expression. What is your intent with the function? I.e. what type does it take in, and what type does it return? Is it supposed to be a Function<E, R>? Commented Nov 14 at 22:57
  • The function should accept a collection (type C - in this case a List.class) with elements of type E - so List<E> - and return a list of strings (R would be List<String> ). There is no generic parameter for String. Commented Nov 14 at 23:06
  • Just added more info - including a "fix" that works, but removes our compile time type-safety... Commented Nov 14 at 23:08
  • Does the downstream need both the Class<List> and Class<MyObj> parameters? Or just the Class<List>? Commented Nov 15 at 0:10
  • 2
    Root cause issue: Generics is not reified. Hacking around it with having Class as parameter just doesn't work. In other words: Mix generics and j.l.Class parameters? That's the root cause. Don't do that. Unfortunately, there is no single '.. just do this instead' answer. It depends on your API. Point is, you're heading in the wrong direction with this API. If you insist on keeping it, you're going to have to accept workarounds, casts that cause warnings, less compile time checking than you wanted, and wonky differences between ecj and javac inference engines. Commented Nov 15 at 17:13

3 Answers 3

3

The compiler must infer the type parameter E to be Object. E cannot be inferred as MyObj because then List.class cannot be passed to a parameter expecting a Class<? extends List<MyObj>>.

The simplest workaround is to just cast List.class to the correct type before passing it

(Class<List<MyObj>>)(Object)List.class

Of course, this means you would be writing List and MyObj two times each.


If you need both Class objects for later and don't like repeating yourself, one way I can think of is taking a "type token" object, such as com.google.common.reflect.TypeToken.

// The 'E' type parameter isn't actually necessary anymore
public static <C extends Collection<E>, E, R> MyFunctionHolder<C, R> forCollectionOfType(
        TypeToken<C> collectionType,
        Function<C, R> function
){
    // you can get the two Class objects this way
    Class<? super C> collectionClass = collectionType.getRawType();
    Class<?> elementClass = collectionType.resolveType(Collection.class.getTypeParameters()[0]).getRawType();
    System.out.println(collectionClass);
    System.out.println(elementClass);

    return new MyFunctionHolder<>(function);
}

Caller:

MyFunctionHolder<List<MyObj>, List<String>> fh = forCollectionOfType(
        // the type in <...> can be inferred from the type of 'fn'!
        new TypeToken<>() {},
        l -> l.stream()
                .map(o -> o.getArg1())
                .toList()
);

A notable difference between taking a TypeToken vs Class is that you have moved some compile time checks to runtime. For example, you are not allowed to pass in a type variable in either case,

public static <T extends Collection<MyObj>> void someMethod() {
    forCollectionOfType(
            new TypeToken<T>() {}, // you cannot do this
            l -> l.stream()
                    .map(o -> o.getArg1())
                    .toList()
    );
}

While T.class will produce a compile time error, new TypeToken<T>() {} would throw at runtime.


If you don't want to use external libraries, you can write something like TypeToken yourself if you only have a limited number of cases to handle. For example,

public static abstract class CollectionTypeToken<C extends Collection<E>, E> {
    private final Class<?> collectionClass;
    private final Class<?> elementClass;

    public CollectionTypeToken() {
        var superType = (ParameterizedType)getClass().getGenericSuperclass();
        collectionClass = switch(superType.getActualTypeArguments()[0]) {
            case Class<?> x -> x;
            case ParameterizedType x -> (Class<?>)x.getRawType();
            default -> throw new UnsupportedOperationException();
        };
        elementClass = switch(superType.getActualTypeArguments()[1]) {
            case Class<?> x -> x;
            case ParameterizedType x -> (Class<?>)x.getRawType();
            default -> throw new UnsupportedOperationException();
        };
    }

    public Class<?> getCollectionClass() {
        return collectionClass;
    }

    public Class<?> getElementClass() {
        return elementClass;
    }
}

This implementation only handles cases where C and E are parameterised types or simple classes. It does not handle the case where they are wildcards or generic arrays. A more proper implementation would involve traversing the Type tree. See the implementation of Guava's TypeToken for other cases.

Sign up to request clarification or add additional context in comments.

4 Comments

This strikes me as overengineered or answering a far more general question as was asked. The solution is to just delete the Class parameters and trust java's inference to take care of it. Which both javac and ecj can do with no pain here. No need to hack around with STTs and such.
@rzwitserloot Well the question says “However, we have downstream reasons for needing the collection class”. Rather than trying to guess what those “downstream reasons” are and doing a frame challenge, I simply answered within the constraints given by the question.
Yes, this is exactly the type of answer I was looking for. I'm the end, we decided that allowing arbitrary collection classes wasn't worth the additional headache to users is the library. So we settled on one collection type (List<E>) and removed the collection type argument from the static method. Of we need to support other collection types in the future, we will just add additional static methods. At the end of the day, our goal is to keep it simple for the framework users to so the right thing - and as much as possible, have compile time enforcement of the right thing.
PS - and I agree that the true solution here is complex - seeing that example and more important understanding it is what allowed is to change scope of what collection types we will support. Very, very helpful - and exactly why SO is a great resource.
1

Mixing Class (as in, a parameter and/or field has the type Class, whatever the generics of that might be) + generics is a very common pattern and in my experience, 99%+ of the time it's wrong. The fundamental issue is that class has a generics param but this is broken - class instances can't actually represent generics. There is no way to obtain an expression of type Class<SomeType<FOO>> where FOO is anything other than ?. For example, Class<List<String>> is a fake construct. It doesn't exist. There is only Class<List>. However, in this code, the generics aren't going to work unless that is a Class<List<E>>, and - voila. Broken.

There is no general answer

The problem is, there is no singular fix. No one thing you can always replace this kind of code with.

The likely solution to your problem

You need to ask yourself why you think you wanted a parameter of type Class, because, you don't. What's the underlying reason? You had problem X, and you needed a solution, and you thought 'I know! I'll make a parameter of type Class, that will solve X!' - that was a logical, but likely incorrect, idea. What's X?

Some examples:

  • X is: "I need to make instances of a type". Your thought was: "I know! an instance of java.lang.Class has a nifty newInstance method, I can use that, grand!". That was wrong. The correct thought was: "I know! The parameter will be a Supplier<C>, and callers will e.g. write List::new instead of List.class".

  • X is: "I want reified generics". That whole question is just wrong. Java does not have it, don't try to hack it. We need to even further up the thought process stack. Why do you think you wnat reified generics?

  • X is: "I just want a way for the caller to specify the types they want first, so that the lambda automatically has the right types". That is also a weird thought. Java already has a type coercion mechanism for lambdas. Don't reinvent the wheel. Just use java's system, which is receiverOrType.<CoercedGenericsGoHere>methodName(lambdaAndOtherArgs);. Often you don't need do that at all. Indeed, in this example case.. you dont!

I'm hoping it's the first one or the third one, because those are easy fixes.

Thus, you end up with...

import module java.base;

class AnotherTestClass {
    public static class MyObj{
        private final String arg1;
        
        public MyObj(String arg1) {
            this.arg1 = arg1;
        }
        
        public String getArg1() { return arg1; }
    }

    public static class MyFunctionHolder<T, R> {
        Function<T, R> f;

        public MyFunctionHolder(Function<T, R> f) {
            this.f = f;
        }
    }
    
// Literally your class doesn't do _anything_ with those types.
// Sometimes, the answer to 'X is annoying me' is 'just delete it then'.
// How convenient! Just get rid of your 2 `Class` parameters!
    public static <C extends Collection<E>, E, R> MyFunctionHolder<C, R> forCollectionOfType(Function<C, R> function) {
        return new MyFunctionHolder<C, R>(function);
    }

// But if you oversimplified and you do need em:
    public static <C extends Collection<E>, E, R> MyFunctionHolder<C, R> forCollectionOfType2(Supplier<C> collectionFactory, Function<C, R> function) {
        C collection = collectionFactory.get();
        E element = collection.iterator().next();
        // generics works fine here - no warnings on the above line!
        return new MyFunctionHolder<C, R>(function);
    }

    void main(String[] args) {
      testMultiLevelStreams();
    }

    public void testMultiLevelStreams() {
        List<MyObj> list = Arrays.asList(new MyObj("one"), new MyObj("two"));
        
        MyFunctionHolder<List<MyObj>, List<String>> fh =
          AnotherTestClass.forCollectionOfType2(
            ArrayList::new, 
            l -> l.stream()
              .map(o -> o.getArg1())
              .toList()
          );
        
        // in fact, that first version just works:
        MyFunctionHolder<List<MyObj>, List<String>> fh2 =
          AnotherTestClass.forCollectionOfType(
            l -> l.stream()
              .map(o -> o.getArg1())
              .toList()
          );

        List<String> rslt = fh.f.apply(list);
        List<String> rslt2 = fh2.f.apply(list);
    }
}

2 Comments

This guidance is solid in general... In our case, we are deserializing data with no intrinsic type information other than the target class (i.e, so we actually, truly, do need the type information downstream).
Create a container, deserialize that. A java type is a vastly superior 'totebag' for generics than any attempts at a runtime API can ever be. As mentioned, List<String> as a concept cannot be represented by a parameter of type java.lang.Class.
0

in short to just outline your error: there is nothing, that somehow narrows down the type of "o" in the definition of forCollectionOfType. The compiler needs to compiler the containing class and does not have any hint, what it will be used for LATER. It can only assume, that its an Object Class.

The fact, that you LATER just use it with a MyObj type variable cannot be known at compile time of AnotherTestClass, so the rult is that the compiler complains about Object not having a method callled getArg1.

Solution: you nee dto narrow down the type "Function<C, R> function" to just accept function that result in an "o" to be of type "MyObj" ... being FULLY generic CANNOT work if you expect it to call a method thats only existing in a sibbling of MyObj

1 Comment

What is interesting is that some compilers' type inference was able to do this properly. I think we are just banging our heads against different inference capabilities of different compilers. In the end, it was more practical to drop support for different types of collections and limit the function to only working on List<E>. I think it was the two levels of inference that were causing the problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.