Java method with generic return Type
Asked Answered
V

2

6

Is there a way in Java to return different types with one declaration of a method?

public Object loadSerialized(String path) {
    Object tmpObject;

    try {
        FileInputStream fis = new FileInputStream(path);
        ObjectInputStream ois = new ObjectInputStream(fis);
        tmpObject = (Object) ois.readObject();

        ois.close();
        fis.close();

        return tmpObject;
    } catch (FileNotFoundException e) {
        return null;
    } catch (Exception e) {
    }
}

I want this method to return an Object and I cloud cast it to the right type at the function call. That was what i thought but it doesn't work like this. Do I need some kind of generic return Type to do this? What would be the best way to solve this problem?

Veiling answered 21/12, 2017 at 18:52 Comment(5)
What doesn't work? What error message are you getting? I notice that the final catch block doesn't return anything.Culet
Provide the usages of loadSerialized method please. From question it is not clear what the problem isTrisomic
Casting at the call site is the right thing to do here.Namesake
Have you tried googling for "java generic functions"?Anacoluthia
Never write an empty catch block. If something goes wrong, you will want to know exactly what happened and where, so you can fix it, not ignore it. Always display the stack trace of a caught exception.Envoi
E
14

To do this safely, you need to pass in the desired type as a Class object:

public <T> T loadSerialized(String path, Class<T> targetType) {
    try (ObjectInputStream ois = new ObjectInputStream(
        new BufferedInputStream(
            new FileInputStream(path)))) {

        Object tmpObject = (Object) ois.readObject();
        return targetType.cast(tmpObject);
    } catch (FileNotFoundException e) {
        return null;
    } catch (IOException | ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

While you could write return (T) tmpObject;, that will generate a compiler warning, because it is not safe: since the compiler only knows that T might be some descendant of Object (or Object itself), the compiler generates (Object), which is the same as doing nothing at all. The code blindly assumes the returned object is of type T, but if it isn’t, when the program tries to call a method defined in T, you’ll get a surprise exception. It’s better to know as soon as you have deserialized the object whether it was the type you expected.

A similar thing happens if you do an unsafe cast on, say, a List:

List<Integer> numbers = Arrays.asList(1, 2, 3);

List<?> list = numbers;
List<String> names = (List<String>) list;  // Unsafe!

String name = names.get(0);    // ClassCastException - not really a String!
Envoi answered 21/12, 2017 at 22:54 Comment(8)
the cast to Object at (Object) ois.readObject(); is obsolete and can be omittedAcceptor
I'm curious to see an example where passing the desired type would make it safe at either compile time or runtime. Yes, the runtime type of T will be Object in this method, but the cast to the actual type of T will occur on the caller side of the function. The bytecode will represent this: String value = (String) loadSerialized(..). Could you perhaps expand on your unsafe list cast example, and illustrate how an explicit cast would make it safer? Or maybe an example using loadSerialized with and without the explicit cast?Eliathas
@Eliathas That bytecode will never be generated. When the definition is <T>, the compiler cannot know to generate a String cast.Envoi
@Envoi Oke well to be precise, at runtime there is no such thing as casting, only checking whether the object is of the correct type, and if it is not, it will throw a ClassCastException. The bytecode instruction that corresponds to a cast is checkcast. Take a look at this answer to see the difference in generated bytecode. You will see that checkcast is performed on the value returned by the generic method. With the explicit cast, this check is still performed (since the method still returns Object), but it is cast inside the method.Eliathas
@Eliathas The checkcast in that bytecode does not correspond to the (T) in that code. It is the implicit cast in the method call, which is effectively String k = (String) convertInstanceOfObject(345435.34);. That is how generics are implemented. Given a List<String> items and the code String item = items.get(0), the compiler generates String item = (String) items.get(0).Envoi
@Envoi Yes, that is what I said right? I said "The bytecode will represent this: String value = (String) loadSerialized(..)". What I mean is that checkcast is there, and it will throw a ClassCastException if it is not the expected type. So my question then is, can you maybe give an example where usage of the method without the class type argument will not trigger an exception, and the method with the class type argument does trigger an exception?Eliathas
The reason why I'm asking is because I'm having a hard time thinking up a case that shows the difference in safety.Eliathas
@Eliathas The difference is an exception occurring where the code makes it clear an exception might occur—a call to Class.cast—versus a method call with no apparent casting taking place. The former is much easier to diagnose.Envoi
I
2

You can use a generic in your return type. It might look something like this. In simple terms, the compiler chooses the best type for T depending on how the method has been called. The casting then happens inside the method, not outside.

Note that I've used the try-with-resources syntax, to avoid messing round with closing streams.

public <T> T loadSerialized(String path) throws IOException, ClassNotFoundException {
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path))) {
        return (T) ois.readObject();
    }
}
Incorruption answered 21/12, 2017 at 18:59 Comment(8)
Casting to a generic type is unsafe and will generate a compiler warning. The method should accept a second argument like Class<T> targetClass and should call targetClass.cast on the deserialized value.Envoi
Yes, that would be a good thing to do. @VGR, you could write your own answer that shows that technique, and I will upvote it. As my answer stands, obviously there will be a compile warning, but it seems to me that that's a warning you'd actually want, due to the inherent unsafeness of expecting deserialization to result in an object of a class of your choosing.Incorruption
@Envoi I don't see how that would be safer. In my opinion it would even be less safe, since the compiler no longer warns you.Eliathas
@Eliathas It’s fail-fast. The (T) will compile, but since T’s upper bound is Object, the actual bytecode will be (Object), which of course is the same as doing nothing at all. The code will assume the object is of type T, but if t wasn’t, you won’t know until the program tries to call a method of T, at which time you’ll get an unexpected exception. Casting it as soon as you deserialize it gives you an immediate, clearer exception, right at the point where the problem occurred.Envoi
@Eliathas Except, those will generate a cast as soon as the object is cast, whereas (T) will not, due to type erasure: the compiler cannot know that T is String.Envoi
@Envoi the actual bytecode will only be (Object) if you call it like Object value = loadSerialized(..). If you capture it in a non-Object type it will look like: String value = (String) loadSerialized(..), which is just as unsafe as String value = (String) loadSerialized(.., String.class). I agree that with class.cast it will cast within the method which is faster, but still just as unsafe, but with no compiler warning.Eliathas
Could you explain the syntax a little more, please? What does <T> T mean? Which one of the two is a return type?Hamrnand
@Hamrnand it's a generic method - explained here. The <T> is a type parameter, so it can mean any type. The T after that is the return type - it just means that the return type of the method will be the same as the type parameter. The compiler figures out what the type parameter is, based on the context in which the method is called. That means that this method will return whatever type it needs to return - and if ois.readObject() doesn't return an object of the right type, the method will fail at run-time.Incorruption

© 2022 - 2024 — McMap. All rights reserved.