Nicole
Nicole

Reputation: 33197

Need to filter a list to a particular subclass using generics

I have a List that contains a certain superclass (like Vehicle), and I would like to write a method that returns the objects in that list that are instances of a certain subclass (like Car).

So far I have this, but it generates a typical "unchecked" operation compiler warning:

public <T extends Vehicle> List<T> getVehiclesOfType(Class<T> type) {
    List<T> result = new ArrayList<T>();

    for (Vehicle vehicle : getVehicles()) {
        if (type.isAssignableFrom(vehicle.getClass())) {
            result.add(type.cast(vehicle)); // Compiler warning here
            // Note, (T)vehicle generates an "Unchecked cast" warning (IDE can see this one)
        }
    }

    return result;
}

Warning: Note: Test.java uses unchecked or unsafe operations.

I'm ok with any other method of accomplishing this (I couldn't find anything in Collections, but it's possible some JDK method can do it), but ideally it would provide the following interface:

List<Car> cars = getVehiclesOfType(Car.class);

I would like to know why I was receiving a compiler warning on the original code, though.

Upvotes: 8

Views: 3740

Answers (8)

Bruno De Fraine
Bruno De Fraine

Reputation: 47296

Multiple answers to this question claim that the warning is due to the fact that the compiler can't statically verify that the cast cannot fail. Although the compiler indeed cannot verify that, that's no reason to give a warning! If the compiler would flag warnings for all casts that can't be verified statically, that banishes all meaningful casts, because when a cast can be verified statically, you generally don't need to be doing the cast in the first place. In other words, the entire point of a cast operation is that it can fail at runtime, which is a perfectly fine situation that is handled with a ClassCastException.

The "unchecked cast" warning on the other hand, happens when you do a cast for which there is insufficient type information to verify the cast at runtime. This can happen because of the erasure of type arguments at runtime. Suppose that you cast something of static type List<?> to List<Vehicle>. At runtime, objects of both of these types simply have the class ArrayList or LinkedList as their only runtime type information, without type arguments. So the compiler can't insert any code that will verify at runtime that the object is indeed a List of Vehicle. So the compiler does nothing, but raises an "unchecked cast" warning.

This is a useful warning, because you may run into a problem when you start using the result of the cast. Since the result has static type List<Vehicle>, you may write code that treats elements from the list as a Vehicle, without having to write a cast. But there is actually still a cast at runtime, and it will fail when the List turns out to contain anything that is not a Vehicle. So you may get a ClassCastException at a point where you did not expect it.

The safe way to handle such a conversion from List<?> to List<Vehicle>, is to iterate over each of the elements, cast them to Vehicle (something you can verify at runtime and which will raise a ClassCastException at a well defined point), and add them to a new list. I wrote some generic code to do precisely that in this answer.

Upvotes: 0

bestsss
bestsss

Reputation: 12056

2 things:

I would like to know why I was receiving a compiler warning on the original code, though.

1st: Look at the code of Class: it does hide the cast for you. Normally casting to any arbitrary type (T) should be a warning but Class.cast actually checks and ignores the compiler warning(nonsense).

public T cast(Object obj) {
    if (obj != null && !isInstance(obj))
        throw new ClassCastException();
    return (T) obj;
}

2nd That being said: Generics warnings is the 1st thing to disable. Having mixture of old code and now just doesn't worth suppress warning, nor I'd care. I am just waiting to see how generics reduce ClassCastException and it's probably true in only one case: using add instead addAll (put/putAll)

Upvotes: 1

Eric Giguere
Eric Giguere

Reputation: 3505

Personally, I think you're taking the wrong approach. Generics in Java are a compile-time only feature. You know what kind of list you need at compile-time so just create a helper method that returns the right kind of list:

public static List<Car> getCars( List<Vehicle> vlist ){ /* code here */ }

Add the helper method to the class in question and then in your code just do:

List<Car> cars = Cars.getCars( getVehicles() );

No casting issues at all. A bit more code to write, but you could also create overloaded versions that returned only blue cars, only red cars, etc.

Upvotes: 0

Jochen Bedersdorfer
Jochen Bedersdorfer

Reputation: 4122

You are operating on Vehicle, which is a superclass and you try to downcast to a subclass of Vehicle. This is always an unsafe cast and will earn you a compiler warning.

So while you can cast from Car to Vehicle without warning (since Car extends Vehicle), the compiler has no way of knowing that the variable typed as Vehicle is actually a car.

By using a cast (either by using (Car) or cast(..)), you are telling the compiler that you know better. The compiler hates people and still hands out a warning to you :)

Upvotes: 0

Zeki
Zeki

Reputation: 5277

The problem is that the compiler isn't smart enough to know that vehicle is of class "type". This is a runtime check and the compiler doesn't do that kind of analysis. There are lots of situations like this. For example I use if (true) return; to exit out of a function early during debugging all the time. If I use just return, the compiler realizes that there is unreachable code, but with the conditional, the compiler doesn't realize that it's impossible to get into that branch.

Consider if you replace your conditional with if (false) {. The code has no chance of throwing an exception but still contains an unsafe cast.

Basically the compiler is saying, "I can't confirm that this is safe so it's up to you to make sure you know what you are doing." Your code isn't broken, you just need to exercise caution.

Upvotes: 1

David Moles
David Moles

Reputation: 51093

You're getting a warning because there's no way for the compiler (or the IDE) to know that the cast is safe, without understanding the meaning of isAssignableFrom(). But isAssignableFrom() isn't a language feature, it's just a library method. As far as the compiler's concerned, it's the same as if you'd said

    if (type.getName().contains("Elvis")) {
        result.add(type.cast(vehicle));
    }

However, you know what isAssignableFrom() means, so you know it's safe. This is exactly the sort of situation @SuppressWarnings is meant for.

Upvotes: 6

sblundy
sblundy

Reputation: 61414

You might be stuck with adding @SuppressWarning("unchecked") to the method

Upvotes: 0

Sean Patrick Floyd
Sean Patrick Floyd

Reputation: 298838

How about this?

if (type.isInstance(vehicle)) {
    result.add((T)(vehicle));
}

Does the compiler still complain like that?


But if I were you I'd use Guava, that will make your method a one-liner:

public <T extends Vehicle> List<T> getVehiclesOfType(Class<T> type) {
    return Lists.newArrayList(Iterables.filter(getVehicles(), type));
}

Upvotes: 3

Related Questions