diff --git a/kotlinx-coroutines-core/jvm/src/Exceptions.kt b/kotlinx-coroutines-core/jvm/src/Exceptions.kt index 6175595713..a7519862e0 100644 --- a/kotlinx-coroutines-core/jvm/src/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/src/Exceptions.kt @@ -61,6 +61,10 @@ internal actual class JobCancellationException public actual constructor( override fun equals(other: Any?): Boolean = other === this || other is JobCancellationException && other.message == message && other.job == job && other.cause == cause - override fun hashCode(): Int = - (message!!.hashCode() * 31 + job.hashCode()) * 31 + (cause?.hashCode() ?: 0) + + override fun hashCode(): Int { + // since job is transient it is indeed nullable after deserialization + @Suppress("UNNECESSARY_SAFE_CALL") + return (message!!.hashCode() * 31 + (job?.hashCode() ?: 0)) * 31 + (cause?.hashCode() ?: 0) + } } diff --git a/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt b/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt index c063e9457e..18c3d29db5 100644 --- a/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt +++ b/kotlinx-coroutines-core/jvm/test/JobCancellationExceptionSerializerTest.kt @@ -36,4 +36,30 @@ class JobCancellationExceptionSerializerTest : TestBase() { finish(4) } } + + @Test + fun testHashCodeAfterDeserialization() = runTest { + try { + coroutineScope { + expect(1) + throw JobCancellationException( + message = "Job Cancelled", + job = Job(), + cause = null, + ) + } + } catch (e: Throwable) { + finish(2) + val outputStream = ByteArrayOutputStream() + ObjectOutputStream(outputStream).use { + it.writeObject(e) + } + val deserializedException = + ObjectInputStream(outputStream.toByteArray().inputStream()).use { + it.readObject() as JobCancellationException + } + // verify hashCode does not fail even though Job is transient + assert(deserializedException.hashCode() != 0) + } + } }