简体   繁体   中英

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.

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 .

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;
} 

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.

There is no problem in declaring a return type that is an abstract class. That's an important tool in Object Oriented Programming. Eg, 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, eg 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM