Frank Haubenreisser
Frank Haubenreisser

Reputation: 337

Return an object of child class from abstract base class

I have classes that I want to (de-)serialize. I don't want the code to appear in every class, so I thought I'll make child classes like this

public class Swan extends Animal {}

and a base class like this:

public abstract class Animal {
   protected String name;
   // ...


   public void saveAnimal(String filename) {
       //ObjectOutputStream, save name...
   }

   public static /*returntype*/ loadAnimal(String filename) {
       //ObjectInputStream...
   }
}

Basically I want this to work:

Swan s1 = new Swan("name");
s1.saveAnimal("/pathToSaveFile");
Swan s2 = (Swan)loadAnimal("/pathToSaveFile") OR 
Swan s2 = loadAnimal("/pathToSaveFile")

How do I do this if Animal is abstract? If the method looks like this:

public static <T extends Animal> T loadAnimal(String filename) {
    //loadFromFile
    }

I cannot return new T(name) //parameter cannot be instantiated directly. I read a bit about reflection but you cannot get the class of a generic type T. The workaround for this is to pass the class of the type into a constructor, but Animal should be abstract.

Upvotes: 6

Views: 5925

Answers (4)

Jim Garrison
Jim Garrison

Reputation: 86764

Due to type erasure you can't do exactly what you want, but here is some code that shows three options:

public class Foo {

    public static class Animal {
        public void save(String filename)
        {
            // Write to file
        }
        public static <T extends Animal> T load(String filename, Class<T> cls) throws Exception
        {
            T result = cls.newInstance();
            // initialize result
            return result;
        }
    }

    public static class Swan extends Animal {
        public static Swan load(String filename) throws Exception
        {
            return load(filename, Swan.class);
        }
    }

    public static void main(String[] args) throws Exception
    {
        Swan s = new Swan();
        s.save("somefile");
        Swan s2 = Swan.load("somefile", Swan.class);
        // OR equivalently
        Swan s3 = Animal.load("somefile", Swan.class);
        // OR also equivalent
        Swan s4 = Swan.load("somefile");
    }
}

In order to instantiate T you have to have access to the Class object so you can do newInstance(). This requires a no-arg constructor or some more code to find and invoke the proper constructor, but that's basic reflection. You can hide the need for the Class if you provide a load method in Swan. Note that this is not an override, as inherited static methods don't participate in polymorphism. Swan.load merely hides the equivalent method from Animal.

Upvotes: 4

Holger
Holger

Reputation: 298123

There is no problem in declaring a return type that is an abstract class. That’s an important tool in Object Oriented Programming. E.g., the methods Arrays.asList(…), Collections.emptyList(), Collections.unmodifiableList(…) have in common that their return type is List, an interface. This does not imply that List in instantiated, as that’s impossible. It just implies that an unspecified subtype (implementation) of that interface is returned.

So you can simply declare your method to return Animal and let it return whatever the deserialization produced, if casting the object to Animal succeeded, it is a concrete subclass of Animal, perhaps a Swan.

If you want to remove the type cast at the caller side, you can provide the expected type as a parameter

public static <T extends Animal> T loadAnimal(String filename, Class<T> type) {
    //loadFromFile
    return type.cast(loadedObject);
}

which you can invoke as

Swan s2 = loadAnimal("/pathToSaveFile", Swan.class);

However, this does not provide more safety than an explicit type cast. There is no way to guaranty at compile time that a particular object loaded from an external file will contain a Swan rather than an arbitrary other animal.

This assumed that you deserialized the entire Animal instance, rather than properties of it. Otherwise, you could use the Class object to create the proper instance, e.g. via newInstance before reading its properties. But note that this is a questionable design. Normally, you create different subclasses, because they are going to have different properties.

Upvotes: 0

Oleg Imanilov
Oleg Imanilov

Reputation: 2751

The simplest way to solve it - create wrapper which contains instance type (Specific animal class) and the serialization body. So you will load type first, than instantiate and read the right class.

Upvotes: 0

if you do this and then check the instance then it will work...

public static Animal  loadAnimal(String filename) {
    //ObjectInputStream...
}

Example:

Swan s1 = new Swan("name");
s1.saveAnimal("/pathToSaveFile");
Animal s2 =  loadAnimal("/pathToSaveFile") //it is ok since Swan is an animal..
if(s2 instanceOf Swan){
    Swan sccc = (Swan)s2;
} 

Upvotes: 0

Related Questions