7

Why do collections that are not related to the template class drop their type? Here is an example: (Sorry, it will not compile because of the error I'm confused about.)

package test;

import java.util.ArrayList;
import java.util.List;

public class TemplateTest {

    public static class A { }

    public static class B<T extends Comparable> {
        List<A> aList = new ArrayList<A>();

        public List<A> getAList() {
            return aList;
        }

        public int compare(T t, T t1) {
            return t.compareTo(t1);
        }
    }

    public static void main(String[] args) {
        B b = new B();
        for (A a : b.getAList()) { //THIS DOES NOT WORK

        }
        List<A> aList = b.getAList(); //THIS WORKS
        for (A a : aList) {

        }
    }
}

This code throws an error upon compilation:

test/TemplateTest.java:24: incompatible types
    found   : java.lang.Object
    required: test.TemplateTest.A
        for (A a : b.getAList()) {

If I specify the template of B like B<String>, or if I remove the template from B completely, then everything is ok.

What's going on?

EDIT: people pointed out there was no need to make B generic so I added to B

naugler
  • 1,060
  • 10
  • 31
  • 1
    Sorry about the nonsense before... This seems to be related to a bug. Kudos! – zw324 Feb 14 '13 at 19:08
  • 1
    Not strictly related to your question, but *not* specifying the type parameter in Java is generally considered a programming error. Using the raw type only compiles as a concession to backward compatability. – Affe Feb 14 '13 at 19:14
  • I call compiler bug as well. It seems like the type of the right-hand-side expression used in an enhanced for loop is determined in a Special way (not surprising since it needs to special-case arrays), and this breaks when a raw type is involved. (I.e. seeing a raw type somewhere, it assumes the whole expression has types erased.) – millimoose Feb 14 '13 at 19:25

3 Answers3

8

Yes, it is known behaviour that if you use a raw type, then all type parameters on the class are lost, not just the type-level parameter that you failed to declare.

The issue is partly here:

If I specify the template of B like B<String>, or if I remove the template from B completely, then everything is ok.

That's not an option, you aren't to choose if you want to specify the type parameter or not. The only reason it compiles at all with no parameter specified is for backward compatibility. Writing new code with missing type parameters is a programming error.

List<A> list = b.getList() does not successfully interpret the type, it is just effectively sticking in an arbitrary cast and trusting you that the assignment is correct. If you look at the compiler warnings it is in fact generating a warning for an unsafe conversion.

for(A a : b.getList()) {} upgrades that warning to an error because the inserted cast would be inside compiler generated code, so it refuses to auto-generate unsafe code at all, rather than just give a warning.

From the java language specification:

The use of raw types is allowed only as a concession to compatibility of legacy code. The use of raw types in code written after the introduction of genericity into the Java programming language is strongly discouraged. It is possible that future versions of the Java programming language will disallow the use of raw types.

Bottom line really is that the only significant thing java generics share with C++ templates is the <> syntax :)

More details: What is a raw type and why shouldn't we use it?

Community
  • 1
  • 1
Affe
  • 47,174
  • 11
  • 83
  • 83
  • But Sun accepts the bug I mentioned in http://stackoverflow.com/a/14882182/688653. So is this a bug or the right behavior? – zw324 Feb 14 '13 at 19:33
  • Okay but this doesn't really explain why the expression type is determined correctly *outside* of the context of an enhanced for-loop. – millimoose Feb 14 '13 at 19:34
  • @millimoose I don't think it is determined correctly outside, but I get a warning instead of an error. eg `List list = notDeclared.getAList();` gives me an 'unchecked conversion' warning. – sharakan Feb 14 '13 at 19:37
  • @millimoose It doesn't work outside the enhanced forloop, you still get the correct compiler warning. It's just that being in the for loop upgrades that warning to an error due to the way that construct is specified. (Basically if it allowed that to compile, there would be the potential for a runtime classcast exception to come out of the expanded version of the loop that the compiler writes in, which it does not like there being a risk of, so it tells you no.) – Affe Feb 14 '13 at 19:38
  • @sharakan Ah. That bit of information is rather essential here. So it seems that an expression involving raw types is completely erased. – millimoose Feb 14 '13 at 19:41
  • @Ziyao Wei search me, ask an engineer at oracle :) That log is 5 years old and marked as an enhancement. One assumes they are in no hurry to bother changing behaviour that's related to the use of raw types regardless. – Affe Feb 14 '13 at 19:42
  • Very interesting, thank you. Had I understood what I was doing was declaring a 'raw' type I may have been able to find this answer, but I am nevertheless surprised by the behavior. While the use of raw types may be discouraged, I do not see why, when providing backward compatibility, they would drop other type parameters. I accept the problem but not the cause! – naugler Feb 14 '13 at 20:26
1

First, in Java, it's Generics, not Templates like it is in C++.

You are declaring a generic type parameter T in your class B but you aren't using it. You should use T instead of A throughout your B class defintion.

public static class B<T> {
    List<T> aList = new ArrayList<T>();

    public List<T> getAList() {
        return aList;
    }
}

Then you should use a type parameter when you declare your instance of class B.

B<A> b = new B<A>();

But if you know that your aList variable will always hold objects of type A as the method name getAList suggests, then there would be no reason to make class B generic.

rgettman
  • 176,041
  • 30
  • 275
  • 357
  • I can use T elsewhere, but even if I do it screws up A if I don't specify T. Why is that? – naugler Feb 14 '13 at 18:59
  • Specifying a generic type parameter for class B means that its "getAList" method will return a List of a specific type "T", but we don't care what type it is yet. The type of "T" is not specified until an instance is created. When you do create your instance "b" of type "B", you specify the type parameter "A", so that when you call "getAList", you get a List back. – rgettman Feb 14 '13 at 19:03
  • No, I want the list to be of type A. The type T is used elsewhere. Why does my list of A objects drop it's type? (I added to my example to show a possible use of T, sorry it's crude) – naugler Feb 14 '13 at 19:05
  • I think I understand your question more thoroughly, I just saw the raw type "B" and leaped to a conclusion about your error. I don't know why it doesn't recognize the type "A" on getAList. – rgettman Feb 14 '13 at 19:13
1

This is really similar to this bug:

6760983 : Unused type parameter triggering error in unrelated method

Which is reported here:

Unused Generic causing problem

zw324
  • 26,764
  • 16
  • 85
  • 118
  • But the OP's code isn't subclassing, so there's no overriding or name clashes as the bug suggests. – splungebob Feb 14 '13 at 19:17
  • True, but in that bug, the additional type parameter confused the compiler so that the type checking on two (apparently) matching types failed, just as in here. Maybe they are related? Another thought: maybe another implementation of Java can help here? – zw324 Feb 14 '13 at 19:25
  • @ZiyaoWei You could try the Eclipse compiler instead of the Sun one. – millimoose Feb 14 '13 at 19:26
  • Well, Affe's answer is pretty much the one, but I will treat this as a bug unless somebody could give us a quote from JLS telling otherwise. – zw324 Feb 14 '13 at 19:37