https://kotlinlang.org logo
#getting-started
Title
# getting-started
y

y

10/05/2023, 7:24 AM
hey, a bug report was submitted to me with something that looks like this:
Copy code
sealed class Foo : SomeInterface1, SomeInterface2 {
    @kotlinx.serialization.Serializable
    object A : Foo() {
        private fun readResolve(): Any = this
        override fun hashCode() = /* ... */
    }

    @kotlinx.serialization.Serializable
    object B : Foo() {
        private fun readResolve(): Any = this
        override fun hashCode() = /* ... */
    }

    @kotlinx.serialization.Serializable
    object C : Foo() {
        private fun readResolve(): Any = this
        override fun hashCode() = /* ... */
    }

    fun doTheThing(baz: List<String>): String = when (this) {
        A -> /* ... */
        B -> /* ... */
        C -> /* ... */
    }
}
the bug report was a
kotlin.NoWhenBranchMatchedException
thrown from
doTheThing()
. how could this be? is this necessarily a kotlin bug?
s

Sam

10/05/2023, 7:32 AM
I have sometimes seen issues like this when using mocks in tests. If a mocking framework is generating an implementation of
Foo
, it could end up with an instance that isn't one of the required subclasses.
y

Youssef Shoaib [MOD]

10/05/2023, 8:40 AM
Use
data object
instead of object. What's happening is that the object is being recreated I think
👍 1
y

y

10/05/2023, 8:59 AM
@Youssef Shoaib [MOD] I didn't know about that feature, interesting. however, going over the feature description, it seems like this is only syntax sugar for auto-implementing
toString()
,
equals()
and
hashCode()
, right? so if if we already implement
hashCode()
(hopefully correctly) and even
readResolve()
, would there be any difference with using a
data object
? @Sam thanks - no mocking involved afaik
y

Youssef Shoaib [MOD]

10/05/2023, 9:00 AM
equals
is what matters here I believe. That's why the when is failing
Try replacing
A ->
B->
etc with
is A->
is B->
and that should also work, because a data object's equals simply does an
is
check
y

y

10/05/2023, 9:08 AM
oh, I see. is this fixable without going to
1.9
(which we might not want to do - not my decision)? is it as simple as adding the following boilerplate?
override fun equals(other: Any?) = hashCode() == (other as? A)?.hashCode()
(and same for
B
and
C
)
or even
override fun equals(other: Any?) = other is A
(and same for
B
and
C
)
...and if that works, then surely your
is A ->
solution would work without
1.9
y

Youssef Shoaib [MOD]

10/05/2023, 9:13 AM
Yes the is check should do the trick, but make sure you override hashcode as well
y

y

10/05/2023, 9:14 AM
@Youssef Shoaib [MOD] just to be clear - the minimal change here would be to add
is A ->
branches in the
when
? no
equals()
override required?
y

Youssef Shoaib [MOD]

10/05/2023, 9:15 AM
Yes that's the most minimal change you can make, just
is A ->
y

y

10/05/2023, 9:16 AM
I see. thanks a lot.
k

Klitos Kyriacou

10/05/2023, 9:56 AM
By the way, comparing hashcodes is not the right way to implement equals. Two objects are allowed to have the same hashcode even when they're not equal.
y

y

10/05/2023, 9:57 AM
@Klitos Kyriacou yes, however the
as?
check would then return
null
in that case (which means, the actual equality here is on the type and the check with the hashcode is superfluous)
k

Klitos Kyriacou

10/05/2023, 10:00 AM
Even then, if
this.hashCode()
happens to return 0, it will equal
null.hashCode()
so will give an incorrect
true
result.
y

y

10/05/2023, 10:00 AM
woah, that's right!
k

Klitos Kyriacou

10/05/2023, 10:03 AM
Actually, I misread it:
(other as? A)?.hashCode()
would be
null?.hashCode()
so it will be comparing
0
with
null
, which is
false
so it would give the right answer anyway.
👍 1
e

ephemient

10/05/2023, 2:19 PM
object
doesn't override
equals
, so it doesn't rely on `hashCode`; it's purely the default
equals
, which is instance-based
there should not be any way of creating instances of
object
in pure Kotlin, but it can happen through Java serialization or reflection
(which
mockk
and other mocking libraries do under the covers)
y

y

10/05/2023, 2:22 PM
so (as previously mentioned), would
is
be the preferred way of making sure all instances of the same `object`s test the same?
e

ephemient

10/05/2023, 2:23 PM
it's a bit awkward but yes
y

Youssef Shoaib [MOD]

10/05/2023, 2:24 PM
Easiest way is to use
data object
it handles all of that because it overrides equals and hashcode
e

ephemient

10/05/2023, 2:24 PM
you can do some tricks with
readResolve
to ensure Java serialization doesn't return non-singleton instances, if you think that's what your users are running into, but there's no way to protect against reflection
oh wait I notice you already have
readResolve
that doesn't look like the right way to do it though
y

y

10/05/2023, 2:29 PM
we do believe this is a serialization thing (by "we", I mean people here more knowledgable about this than me)
e

ephemient

10/05/2023, 2:30 PM
currently your readResolve is simply passing through the newly-created instance. instead it should look like
Copy code
object A {
    private fun readResolve(): Any = A
}
if you want to ensure that only the singleton
A.INSTANCE
exists (after the serialization machinery)
also, it's an API/ABI change, but if you wrote
Copy code
enum class Foo : SomeInterface1, SomeInterface2 {
    A,
    B,
    C,
}
then Java serialization will just work
y

y

10/05/2023, 2:40 PM
@ephemient that
readResolve()
implementation is probably it! thanks for catching it
2 Views