-1

I was answering Applying functions of various types to a value question yesterday while I noticed something quite strange.

I'm able to define a collection as being of type O even though its real type is T. This can probably be explained by the fact that I'm using raw types.

However, the most surprising part is that I'm able to consume from this collection which is a List<FileInputStream> even though it is obvious it is a List<Integer>.

Hereunder is the code in question

public static void main(String[] args) {
    String value = "a string with numb3r5";
    Function<String, List<String>> fn1 = List::of;
    Function<List<String>, String> fn2 = x -> x.get(0);
    Function<String, List<Integer>> fn3 = x -> List.of(x.length());

    InputConverter<String> converter = new InputConverter<>(value);
    List<FileInputStream> ints = converter.convertBy(fn1, fn2, fn3);

    System.out.println("ints = " + ints);
    System.out.println("ints.get(0) = " + ints.get(0));
    System.out.println("ints.get(0).getClass() = " + ints.get(0).getClass());
}

public static class InputConverter<T> {
    private final T src;

    public InputConverter(T src) {
        this.src = src;
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    public <R> R convertBy(Function... functions) {
        Function functionsChain = Function.identity();

        for (Function function : functions) {
            functionsChain = functionsChain.andThen(function);
        }

        return (R) functionsChain.apply(src);
    }
}

And here is the result

ints = [21]
ints.get(0) = 21
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.io.FileInputStream (java.lang.Integer and java.io.FileInputStream are in module java.base of loader 'bootstrap')
    at Scratch.main(scratch_8.java:18)

Why is it possible to consume from this collection ?

I noticed the follwing don't throw an exception when consuming it

System.out.println("String.valueOf(ints.get(0)) = " + String.valueOf(ints.get(0)));
System.out.println("((Object) ints.get(0)).toString() = " + ((Object) ints.get(0)).toString());

However the following does

System.out.println("ints.get(0).toString() = " + ints.get(0).toString());
Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89
  • What exactly is your question? – Turing85 Apr 04 '21 at 10:48
  • @Turing85 I just edited it to add a question at the end. – Yassin Hajaj Apr 04 '21 at 10:55
  • "*Why is it poossible to consume from this collection**" because `+` is defined for the pair `(String, Object)` (i.e. the exact type of the list-element does not matter). But if we call some method on the list-element under the assumption that is is a `FileInputStream`, its type must be assured by a typecast. – Turing85 Apr 04 '21 at 10:56
  • Ok but when I try `ints.get(0).toString()` I get a `ClassCastException` even though it is a method from `Object` – Yassin Hajaj Apr 04 '21 at 10:58
  • ... because you access the list-element **as an actuall** `FileInputStream` (that is what the generic type of the list asserts you, remember: the method to call is determined by the **static** type of the parameters) and the assertion must be enforced by a type cast. This is why the original program works if we replace `System.out.println("ints.get(0).getClass() = " + ints.get(0).getClass());` with `Object o = ints.get(0); System.out.println("ints.get(0).getClass() = " + o.getClass());` ([Ideone demo](https://ideone.com/DKhu8a)). – Turing85 Apr 04 '21 at 11:03
  • Thanks @Turing85 so if I understand correctly, the object is considered ot type `Object` unless you're invoking a method of `Object` on it, then it tries to do a type cast. This is very strange, I'd expect a method of `Object` to not require a type cast.. – Yassin Hajaj Apr 04 '21 at 11:08
  • The tricky thing is that the JLS extremele vague about type assurance. It basically states that the type must be assured by a cast when it is necessary (I do not remember the exact words, would have to look it up). This can lead to scenarios where one and the same call (like shown in the example) sometimes leads to no exception (if a type assuruance is not necessary) and sometimes leads to an exception (if a type assurance is necessary). tl;dr: [do not use raw types](https://stackoverflow.com/questions/2770321/what-is-a-raw-type-and-why-shouldnt-we-use-it). – Turing85 Apr 04 '21 at 11:08
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/230723/discussion-between-turing85-and-yassin-hajaj). – Turing85 Apr 04 '21 at 11:09

2 Answers2

1

Generics are not part of the binary representation of the java classes. Generics are implemented with casting.

“Behind the scenes, the compiler replaces all references to T in Crate with Object. In other words, after the code compiles, your generics are actually just Object types. ” Excerpt From: Jeanne Boyarsky. “OCP Oracle Certified Professional Java SE 11 Developer Complete Study Guide”. Apple Books.

This means the compiler will ensure that convertBy return List, but will not be able to confirm all types. But compiler will replace: ints.get(0).toString() with ((FileInputStream) ints.get(0)).toString()) in case of needs.

A similar example, cast list with the wrong generic parameter will produce the same exception.

    List list = List.of("text");
    List<Integer> numbers = (List<Integer>) list;
    System.out.println(numbers.get(0).longValue());

Conclusion: If we want to have type-safe code we have to ensure that we do not use raw types.

Akif Hadziabdic
  • 2,692
  • 1
  • 14
  • 25
1

The InputConverter#convertBy is using raw type Function declaration as its input and its return type is using generic type inference <R> hence it will resolve to the assigned type, in your case it will be List<FileInputStream>.

This being said, you can assign your converted type, i.e. the result of InputConverter#convertBy to any type you want: List<FileInputStream>, List<HashSet<FileInputStream>>, List<Charset>... as long as it is runtime-compatible to the results of the converted type (the last function result) you would still get no error until, at runtime, the JVM will perform an implicit cast toward the collection erased type in your case being FileInputStream and would fail because of the actual type being Integer.

Below statement won't fail, since you are up-casting the collection element to the Object super type and calling the #toString method from the java.lang.Object type (you are casting the variable before accessing any member / method):

System.out.println("((Object) ints.get(0)).toString() = " + ((Object) ints.get(0)).toString());

Below statement is practically similar to the previous one:

System.out.println("String.valueOf(ints.get(0)) = " + String.valueOf(ints.get(0)));

given the Object#valueOf method implementation is as follows:

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

On the other hand, below statement will fail as the ints.get(0) will be cast (checkcast) to the erased runtime type being FileInputStream before accessing its #toString memeber:

System.out.println("ints.get(0).toString() = " + ints.get(0).toString());

You can think of it as the following:

System.out.println("ints.get(0).toString() = " + ((FileInputStream) ints.get(0)).toString());
tmarwen
  • 15,750
  • 5
  • 43
  • 62