0

Following Example:

I have a service that can 'start' any vehicle

interface VehicleStarterService<T: Vehicle> {
   fun <T : Vehicle> start(vehicle: Class<T>): String {
       return vehicle.start
   }
}

I would like to start a vehicle type by name like "Car", which would require me to create a VehicleStarterService; however I cannot seem to find a way to instantiate an interface with a class that is instantiated by name.

I would like to do something like (but cannot):

val cls = "Car"
val kClass = Class.forName(cls).kotlin
val service = VehicleStarterService<kClass>()
service.start

I end up having to do the following (creating a service for every paramaterized type I need):

class CarStarterService : VehicleStarterService<Car> {
    fun <T> execute() : String {
        return start(User::class.java)
    }
}

Is there any way to instantiate a paramaterized class in this way?

  • 1
    I would say no. Type instantiation must happen at compile time but the type name in a string is only known at run time. Depending on what `start` should really do, maybe generics are not necessary and a common interface for all vehicles plus a class object for the particular vehicle are enough. – Michael Butscher Sep 13 '18 at 19:06
  • What you're going to do is equivalent to a raw type, so just use the raw type. – Louis Wasserman Sep 15 '18 at 22:16

2 Answers2

1

It's not clear if this would be sufficient for your situation, but perhaps you could just match the class based on the string like this

val cls = "Car"
val service: VehicleStarterService<out Vehicle>? = when (cls) {
    "Car"  -> object : VehicleStarterService<Car> {}
    "Boat" -> object : VehicleStarterService<Boat> {}
    "Plane" -> object : VehicleStarterService<Plane> {}
    else   -> null
}
service?.start(...

EDIT: hashmap registry idea to allow some extensibility..

val serviceRegistry = HashMap<String, VehicleStarterService<out Vehicle>>()
    .apply {
        //default services
        this["Car"] = object : VehicleStarterService<Car> {}
        this["Boat"] = object: VehicleStarterService<Boat> {}
    }
.
.
.
//Adding entry
serviceRegistry["Plane"] = object: VehicleStarterService<Plane> {}
.
.
.
//Fetching entry
val cls = "Car"
val service = serviceRegistry[cls]

service?.start(...
luminous_arbour
  • 106
  • 1
  • 4
  • I was hoping to be able to avoid adding a matcher like that (requiring someone else to change an underlying 'library' class) but yes, that's definitely an option! – LostHawkGSW Sep 13 '18 at 22:56
  • I edited in a second idea, using a hashmap as a registry so that someone else could add their own services and fetch those as well – luminous_arbour Sep 14 '18 at 05:02
0

I don't really see your problem. Note that the generic type information is erased at runtime (at least for the JVM-variant which you are obviously using). So it basically doesn't matter which generically typed service you use. You can also just use VehicleStarterService<Vehicle> and it will start all your vehicles. If for any reason you need to call a specific (other!) function you still need to check/cast to your actual type... So I see no benefit in trying to get that generic typed interface.

Having said that there are still methods to do it... (note again: it doesn't really matter... generic type information is erased at runtime...)

Now for my tests I use this entry method:

fun startAllTheVehicles(vararg vehicleTypes: String) {
  startAllTheVehicles(*vehicleTypes.map { Class.forName(it) }
                                   .map { it as Class<out Vehicle> }
                                   .toTypedArray())
}

Note that it as Class<out Vehicle> is NOT ensuring that you have a class of type Vehicle. It just makes sure that your code compiles when trying to call your functions that declare Class<out Vehicle> as a parameter type.

So starting from there you could use one of the following ways to have an actual generic type available. I am assuming you use something like newInstance within your start service.

  1. Sample with the generic type omitted on the interface (that case is rather easy):

    interface VehicleStarterService {
      fun <T: Vehicle> start(vehicleType: Class<T>) = vehicleType.newInstance().start
    }
    

    Sample method that is called by the above entry function using one service to start all vehicles:

    fun <T : Vehicle> startAllTheVehicles(vararg vehicleTypes: Class<out T>) {
      val service = object : VehicleStarterService {}
      vehicleTypes.forEach { service.start(it) }
    }
    
  2. Still using an interface with generic type (note the changes regarding out T, etc.):

    interface VehicleStarterService<T: Vehicle> {
      fun start(vehicleType: Class<out T>) = vehicleType.newInstance().start
    }
    

    Sample method that is called by the entry function using one service to start all vehicles:

    fun <T : Vehicle> startAllTheVehicles(vararg vehicleTypes: Class<out T>) {
      // actually it doesn't really matter which type we use...
      val service = object : VehicleStarterService<Vehicle> {} // you could even use T instead of Vehicle
      vehicleTypes.forEach { service.start(it) }
    }
    

Testing both with the following works as expected:

startAllTheVehicles("Car", "Truck")

Calling it with a type that is actually no Vehicle will give you a ClassCastException at the interface's start-function.

Roland
  • 22,259
  • 4
  • 57
  • 84