16

I found a weird situation when casting generics. I run this code:

class A { }

class B { }

public class Program {

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        List<A> listA = new ArrayList<A>();
        List<?> list = listA;
        ((List<B>)list).add(new B());

        for (Object item : listA) {
            System.out.println(item.toString());
        }
    }
}

It compiles very well (only with warning but without error) and run without any exception and the output was:

B@88140ed

How did I do that? I mean why Java allow me to do such thing? I added an instance of B class to list of As?

It is very bad behaviour of generics. Why it is happening?

BTW, I tried it with Java 7.

EDIT:
What surprised me is that Java only notify the problem with warning that every programmer can ignore it. I know that SuppressWarnings is bad idea, but why Java didn't denied such behavior with error or exception?

In addition, this warning showed always, if you think that your casting is correct you have no choice but to ignore it. But if you think that is good casting and ignore it but it isn't?

nrofis
  • 8,975
  • 14
  • 58
  • 113
  • 15
    "It compiles very well" - well yes, because you've explicitly suppressed a warning... You should only do that when you understand the warning you're suppressing, which it looks like you don't in this case. – Jon Skeet Jun 01 '15 at 08:19
  • I meant without fatal error :). Yes, I know that what the warning tells, but I tried it anyway. I expected to get exception or something like that. – nrofis Jun 01 '15 at 08:20
  • 3
    I think "compiles with warnings (which you've disabled)" is pretty different to "compiles very well". If you only disable warnings you understand, and always make sure you understand any warnings you *do* get, you won't have a problem. – Jon Skeet Jun 01 '15 at 08:21
  • If you try to use it when object class is not that must be - ClassCastException will be thrown – cybersoft Jun 01 '15 at 08:22
  • You were lucky this time. Or actually you were *not*, if you were lucky you would have got a compilation error that would tell you that you're doing something wrong :) – Maroun Jun 01 '15 at 08:22
  • 2
    Well ... Maybe you should add a line `A item = listA.get(0)` (at the end) and see what happens when you run it. Maybe you then understand the warning better. – Seelenvirtuose Jun 01 '15 at 08:28
  • People I know what the warning means. But I don't understand why they didn't shows error instead of warning? – nrofis Jun 01 '15 at 08:31
  • @nrofis What exactly did you expect? A compilatoin error? If yes, what and where? Or an exception when running the program? If yes, what and where? – Seelenvirtuose Jun 01 '15 at 08:34
  • @Seelenvirtuose yes I expected to throw a compiler error. Where? when I do casting. The compiler can (or should) know what the type of the instance when using wildcast. – nrofis Jun 01 '15 at 08:36
  • @nrofis please, can you add to your question that when you remove *@SuppressWarnings("unchecked")* you get the following Warning message "*Type safety: Unchecked cast from List to List*" –  Jun 01 '15 at 08:36
  • @nrofis You are casting the variable `list` which is of type `List>`. The compiler **does not know** the concrete type with which the list was instantiated. If - on the other hand - you cast the variable `listA` to `List`, you will get a compiler error. – Seelenvirtuose Jun 01 '15 at 08:43
  • You should put the SupressWarning at the line you do the declaration. – keiki Jun 01 '15 at 11:34
  • Honestly I think that this is should not compile. I think that Java needs to improve their compiler for cases like this because the warning appears even if it safe casting and give too much power to the programmer. In C# this case will not allowed. – nrofis Jun 01 '15 at 13:45
  • `warning that every programmer can ignore`. it compiles and run. why should there be any thing else but warnings? also, if you choose to ignore warning, that's your problem – njzk2 Jun 01 '15 at 16:44
  • @njzk2 I don't agree with you. Because this warning appears every time you cast a wildcard. You must to ignore it if you think that your casting is correct. But what if you think if the casting is correct but it isn't? – nrofis Jun 02 '15 at 07:43
  • casting a wildcard must be occasional at most. And at this point the compiler warns you that it can't protect you anymore, and it is now up to you to make sure you did things right. – njzk2 Jun 02 '15 at 13:31

5 Answers5

19

Every programming language allows you to shoot yourself into the foot.

In this case, Java is in a dilemma: It could keep the generics information in the bytecode and break millions of lines of existing code or silently drop the generics information after the compiler has do it's utmost to check and keep backward compatibility.

The Java team decided for the latter and introduced Type Erasure - which has its flaws. But if they had broken millions of perfectly fine (if type-wise incomplete) lines of Java code, people would have shown up with pitchforks and burning torches ...

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • So why the compiler show me only warning and not check the types more thoroughly and show error instead? – nrofis Jun 01 '15 at 08:30
  • Your example is simple, and everyone can look at it and spot the error. However, compiler writers can't just say "lets just error on some easy-for-humans-to-locate mistakes"; either they have a general answer, or, if they don't, they just output a warning. Implementing a true general answer would be too hard (and break lots of existing code). So they went for the "lets just warn that it might be wrong" approach. – tucuxi Jun 01 '15 at 08:33
  • 2
    "people would have shown up with pitchforks and burning torches"... Then why didn't they show up when firmly type-safe generics were added to C#? =) –  Jun 01 '15 at 10:01
  • @nrofis: A compiler doesn't come with a brain - bring your own. In pre-generics Java, people sometimes tried to put different types of objects into lists. This isn't an error; it can work if you're very, very, **very** careful. It was "clever coding" which saved memory or tried to avoid GC. In the meantime, most people realized that this is bad and stopped it. – Aaron Digulla Jun 01 '15 at 10:12
  • 2
    @Mints97: I'm not sure. My guess is that they didn't have the same amount of legacy code, or maybe they didn't care. And let's not forget that software developers on Windows are more ma...um...lenient on average ;-) – Aaron Digulla Jun 01 '15 at 10:14
  • @Mints97: because they were added with *other* compatibility quirks, so the old code *does* still work. – Holger Jun 01 '15 at 10:29
  • @Holger: why couldn't Java have added such quirks too, then? =) –  Jun 01 '15 at 11:02
  • 2
    @Mints97: They *could* have done it this way, if loosing Java’s biggest value, the amount of available 3rd party libraries, doesn’t matter. The way Generics were introduced allowed to use existing (not adapted) libraries even inside application that use Generics. You are assuming that C#’s solution is better by looking at a single, possibly overrated, property. Not having full *variance* might hurt much more. Further, having a real enforced Generic type system would imply that even simple things like `Collections.emptyList()` don’t work; it would require a new instance for each type parameter. – Holger Jun 01 '15 at 11:54
  • 1
    @Mints97 C# didn't add generics to existing classes, so it's got a whole pile of classes that nobody uses anymore (i.e. the old collections like ArrayList). – Random832 Jun 01 '15 at 12:21
  • 1
    @Holger and Aaron - I don't buy this argument, although it's written in JLS and repeated by Bloch etc. We can have both reification and raw type. I don't see how adding generic type info in an object hurts binary compatibility with older libraries. My conspiracy theory is that Sun didn't have enough resource and time to push out Java5; so they find PR excuses to justify erasure as a feature instead of a flaw. – ZhongYu Jun 01 '15 at 14:12
  • @bayou.io: it seems, you changed the topic. There is a difference between an implementation which enforces type safety of Generics and an implementation which just adds type information to each instantiation. The latter does indeed not harm compatibility, but I still see the performance impact. As hinted, what is the type of `Collections.emptyList()` or `Function.identity()`? A full reification would require a distinct instance of a distinct type for each site using it with different type arguments. And that’s spreading: `map.entrySet().iterator().next().getReifiedType()` returns *what*? – Holger Jun 01 '15 at 14:28
  • @Holger - I accept the performance argument. But the official `compatibility` argument is very fishy to me. I'm seeking if anyone could elaborate and justify the official argument - https://groups.google.com/forum/#!topic/java-lang-fans/jn6187fWEo8 – ZhongYu Jun 01 '15 at 14:31
  • @bayou.io: I think you should ask a new question for this :-) – Aaron Digulla Jun 01 '15 at 14:45
  • @bayou.io: If generic types were mandatory, you couldn't compile legacy code anymore. As it is, you can compile it (and you get byte code that works) at the cost of lots of generics warnings. There is probably also trouble when you have code paths that go generics -> legacy -> generics -> legacy (callbacks and the like) or when you override method return types. – Aaron Digulla Jun 01 '15 at 15:40
  • @AaronDigulla - to ask an opinionated question on stackoverflow? I'm been scared by mods enough :) – ZhongYu Jun 02 '15 at 03:20
  • @bayou.io: That's not opinonated; just ask what in Java would specifically break if they would compile the generics information into the byte code/types. – Aaron Digulla Jun 02 '15 at 08:09
7

You've defeated the Java compile-time checks through your casting and suppression of warnings.

Note that thanks to type-erasure the list you've created is (under the covers) a simple type-unaware list, and contains no run-time checks or assertions as to what you're putting into it.

Brian Agnew
  • 268,207
  • 37
  • 334
  • 440
3

It is very bad behaviour of generics. Why it is happening?

Because you forced it to happen with the cast, and then made sure that the warnings would be ignored too with your @SuppressWarnings("unchecked").

It's your fault, not the generics mechanism's.

Kayaman
  • 72,141
  • 5
  • 83
  • 121
  • 4
    Well, erasure *is* a pretty nasty limitation of Java generics, and one which I wish didn't exist. I agree that the OP shouldn't suppress warnings without understanding them though. – Jon Skeet Jun 01 '15 at 08:20
3

As others have said, you have circumvented java's type safety.

I will add that your code doesn't explode because your code doesn't require the elements to be anything in particular (just Object). However, had you coded this:

for (A item : listA) { /* whatever */ }

It would have compiled, but would have thrown a ClassCastException at runtime.

Bohemian
  • 412,405
  • 93
  • 575
  • 722
2

You can modify the code for safer:

for (A item : listA) {/* your code here */}

Or

for (Object item : listA) {
                if (item instanceof A) {for (A item : listA) {/* your code here*/}
}

Even if you modify the code as given below, you will get the ClassCastException:

for (Object item : listA) {
   System.out.println(((A)item).toString()); // Here you will get ClassCastException
}
Estimate
  • 1,421
  • 1
  • 18
  • 31