AndroidKotlinNoob
AndroidKotlinNoob

Reputation: 548

How to create an inner class where only the outer class can access the constructor, but the rest is visible everywhere?

I originally wanted to create a class that can abort instantiation in constructor, but according to this link I should instead use a Factory class. But now I want to prevent anyone except the factory class from creating an object of class "Inner" while giving access to the methods of the inner class to everyone.

I have already tried this answer.

import java.util.Date

object InnerFactory {

    class Inner private constructor(startDate: Date? = null, endDate: Date? = null) {
        fun getTimeDifference(): Long? {
            //calculates time difference but doesn't matter to this example
        }
    }

    fun createInnerObject(startDate: Date? = null, endDate: Date? = null): Inner? {
        if (startDate != null && endDate != null && !endDate.after(startDate)) {
            return null
        }

        return Inner(startDate, endDate)
    }

}

I would use it like the following:

val date1 = Date(1547600000)
val date2 = Date(1547600600)
val inner = InnerFactory.createInnerObject(date1, date2) //should return an instance
val invalidInner = InnerFactory.createInnerObject(date2, date1) //should not return an instance because the "endDate" is before "startDate"
val difference = inner?.getTimeDifference()

It says "cannot access '<init>': it is private in 'Inner'" when hovering over my usage of the constructor in the "createInnerObject" function.

Upvotes: 8

Views: 3045

Answers (3)

Willi Mentzel
Willi Mentzel

Reputation: 29844

You could make make the constructor protected. This way you only expose it to subclasses, in this case PrivateClass. Then you will create an instance of PrivateClass or null but return it as InnerClass?.

object InnerFactory {

    fun createInnerObject(startDate: Date? = null, endDate: Date? = null): Inner? {
        // return null here if conditions are not met
        return PrivateClass(startDate, endDate)
    }

    open class Inner protected constructor(val startDate: Date?, val endDate: Date?) {
        fun getTimeDifference(): Long? { /* ... */ }
    }

    private class PrivateClass(startDate: Date?, endDate: Date?): Inner(startDate, endDate)
}

Upvotes: 0

Salem
Salem

Reputation: 14917

Unfortunately, private members of Kotlin inner classes are not accessible from the outer instance:

private means visible inside this class only
Kotlin reference / Visibility modifiers

However, Java is not this restrictive with its visibility modifiers:

access is permitted if and only if it occurs within the body of the top level type (§7.6) that encloses the declaration of the member or constructor.
Java Language Specification / §6 Names / §6.6 Access Control / §6.6.1 Determining Accessibility

This is one of the only (annoying) cases I have found where Kotlin's rules make a common Java pattern impossible.

The only workarounds (if you want to keep your current structure) would be to rewrite this class in Java, or to expose this constructor with a less restrictive visibility (e.g. internal.)

There was a discussion about this on the Kotlin forums - it seems that this is a JVM limitation, and that it only works in Java because the compiler generates appropriate synthetic accessors.

Upvotes: 4

Roland
Roland

Reputation: 23262

What you could do:

  • introduce an interface Inner with all the necessary functions that should be exposed
  • make all the class(es) private and implement that interface

Sample:

object InnerFactory {

  interface Inner {
    fun getTimeDifference(): Long?
  }

  private class InnerImpl(startDate: Date? = null, endDate: Date? = null) : Inner {
    override fun getTimeDifference(): Long? = TODO("some implementation")
  }


  fun createInnerObject(startDate: Date? = null, endDate: Date? = null): Inner? {
    if (startDate != null && endDate != null && !endDate.after(startDate)) {
      return null
    }
    return InnerImpl(startDate, endDate) // InnerImpl accessible from here but not from outside of InnerFactory...
  }
}

Now you can't access InnerImpl from outside anymore, but still have all the necessary functions available:

// the following all work as it deals with the interface
val inner = InnerFactory.createInnerObject(date1, date2) //should return an instance
val invalidInner = InnerFactory.createInnerObject(date2, date1) //should not return an instance because the "endDate" is before "startDate"
val difference = inner?.getTimeDifference()

// the following will not compile:
InnerImpl()

Upvotes: 8

Related Questions