These are the slides of the presentation I gave at the JetBrains HQ by the RAI in Amsterdam named "Decoding Kotlin - Your Guide to Solving the Mysterious in Kotlin". I want to express a big thank you to Xebia and JetBrains for the opportunity. I gave this presentation on the 24th of April 2024.
Decoding Kotlin - Your guide to solving the mysterious in Kotlin.pptx
1. Decoding Kotlin -
Your guide to
solving the
mysterious in
Kotlin
By João Esperancinha 2024/04/24
2. Nullability
1. Working with the Spring Framework
2. Reflection to force nulls
Inline and cross-inline
1. The Java overview
Tail recursive => Tail Cal Optimization
(TCO)
1. What is it
2. Why?
3. How it makes us work recursively
and not use mutable
Data classes
1. Why things work and why things
don't work
2. How to fix the ones that don't
3. How to work with use-site targets.
What does a `delegate` do? and other
use-site targets.
Topics for today
3. Nullability
Kotlin promises a guarantee of
null-safety. Although we can use
nullable members in our classes,
we really shouldn’t whenever
possible.
Whenever possible?
4. @Table(name = "CAR_PARTS")
@Entity
data class CarPart(
@Id
val id: Long,
val name: String,
val productionDate: Instant,
val expiryDate: Instant,
val barCode: Long,
val cost: BigDecimal
)
CREATE SEQUENCE car_parts_id_sequence
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE TABLE CAR_PARTS (
id BIGINT NOT NULL DEFAULT
nextval('car_parts_id_sequence'::regclass)
,
name VARCHAR(100),
production_date timestamp,
expiry_date timestamp,
bar_code BIGINT,
cost float
);
CRUD Entity Example
5. CRUD Entity Example
INSERT INTO CAR_PARTS
(name, production_date, expiry_date, bar_code, cost)
VALUES ('screw', current_date, current_date, 12345, 1.2);
INSERT INTO CAR_PARTS
(name, production_date, expiry_date, bar_code, cost)
VALUES (null, current_date, current_date, 12345, 1.2);
@Test
fun `should mysteriously get a list with a
car part with a name null`() {
carPartDao.findAll()
.filter { it.name == null }
.shouldHaveSize(1)
}
Is this
possible?
6. Reflection Example
val carPartDto = CarPartDto(
id = 123L,
name = "name",
productionDate = Instant.now(),
expiryDate = Instant.now(),
cost = BigDecimal.TEN,
barCode = 1234L
)
println(carPartDto)
val field: Field = CarPartDto::class.java
.getDeclaredField("name")
field.isAccessible = true
field.set(carPartDto, null)
println(carPartDto)
assert(carPartDto.name == null)
println(carPartDto.name == null)
data class CarPartDto(
val id: Long,
val name: String,
val productionDate:
Instant,
val expiryDate: Instant,
val barCode: Long,
val cost: BigDecimal
)
Is this
possible?
7. Inline and
crossinline.
Inline and crossline can be used in
combination with each other.
Inline provides bytecode copies of
the code per each call point and
they can even help avoid type
erasure. Crossinline improves
readability and some safety, but
nothing really functional.
Why does this
matter?
8. Crossinline as just a marker
fun main() {
callEngineCrossInline {
println("Place key in ignition")
println("Turn key or press push button ignition")
println("Clutch to the floor")
println("Set the first gear")
}.run { println(this) }
}
inline fun callEngineCrossInline(startManually: () -> Unit) {
run loop@{
println("This is the start of the loop.")
introduction {
println("Get computer in the backseat")
return@introduction
}
println("This is the end of the loop.")
}
println("Engine started!")
}
fun introduction(intro: () -> Unit) {
println(LocalDateTime.now())
intro()
return
}
public final class IsolatedCarPartsExampleKt {
public static final void main() {
int $i$f$callEngineCrossInline = false;
int var1 = false;
String var2 = "This is the start of the loop.";
System.out.println(var2);
introduction((Function0)IsolatedCarPartsExampleKt$ca
llEngineCrossInline$1$1.INSTANCE);
var2 = "This is the end of the loop.";
System.out.println(var2);
String var4 = "Engine started!";
System.out.println(var4);
Unit var3 = Unit.INSTANCE;
int var5 = false;
System.out.println(var3);
}
public static final void introduction(@NotNull
Function0 intro) {
Intrinsics.checkNotNullParameter(intro,
"intro");
LocalDateTime var1 = LocalDateTime.now();
System.out.println(var1);
intro.invoke();
} public final void invoke() {
String var1 = "Get computer in the
backseat";
System.out.println(var1);
}
Decompiled
code
9. Crossinline as just a marker
fun main() {
callEngineCrossInline {
println("Place key in ignition")
println("Turn key or press pus button ignition")
println("Clutch to the floor")
println("Set the first gear")
}.run { println(this) }
}
inline fun callEngineCrossInline(crossinline startManually: () -
> Unit) {
run loop@{
println("This is the start of the loop.")
introduction {
println("Get computer in the backseat")
startManually()
return@introduction
}
println("This is the end of the loop.")
}
println("Engine started!")
}
fun introduction(intro: () -> Unit) {
println(LocalDateTime.now())
intro()
return
}
public final class IsolatedCarPartsExampleKt {
public static final void main() {
int $i$f$callEngineCrossInline = false;
int var1 = false;
String var2 = "This is the start of the loop.";
System.out.println(var2);
introduction((Function0)(new
IsolatedCarPartsExampleKt$main$$inlined$callEngineCr
ossInline$1()));
var2 = "This is the end of the loop.";
System.out.println(var2);
String var4 = "Engine started!";
System.out.println(var4);
Unit var3 = Unit.INSTANCE;
int var5 = false;
System.out.println(var3);
}
public static final void introduction(@NotNull
Function0 intro) {
Intrinsics.checkNotNullParameter(intro,
"intro");
LocalDateTime var1 = LocalDateTime.now();
System.out.println(var1);
intro.invoke();
}
public final void invoke() {
String var1 = "Get computer in the
backseat";
System.out.println(var1);
int var2 = false;
String var3 = "Place key in ignition";
System.out.println(var3);
var3 = "Turn key or press push button
ignition";
System.out.println(var3);
var3 = "Clutch to the floor";
System.out.println(var3);
var3 = "Set the first gear";
System.out.println(var3);
}
Decompiled
code
11. Tail Call
Optimization
Since the late 50’s TCO was
already a theory intentend to be
applied to Tail Recursivity. It
allows tail recursive functions to
be transformed into iterative
functions in the compiled code for
better performance.
What is the catch?
12. Tail Call Optimization
sealed interface Part {
val totalWeight: Double
}
sealed interface ComplexPart : Part{
val parts: List<Part>
}
data class CarPart(val name: String, val weight:
Double) : Part {
override val totalWeight: Double
get() = weight
}
data class ComplexCarPart(
val name: String,
val weight: Double,
override val parts: List<Part>
) :
ComplexPart {
override val totalWeight: Double
get() = weight
}
data class Car(
val name: String,
override val parts: List<Part>
) : ComplexPart {
override val totalWeight: Double
get() = parts.sumOf { it.totalWeight }
}
tailrec fun totalWeight(parts: List<Part>, acc: Double =
0.0): Double {
if (parts.isEmpty()) {
return acc
}
val part = parts.first()
val remainingParts = parts.drop(1)
val currentWeight = acc + part.totalWeight
return when (part) {
is ComplexPart -> totalWeight(remainingParts +
part.parts, currentWeight)
else -> totalWeight(remainingParts, currentWeight)
}
}
Why use this?
All variables
are immutable
Tail recursive
13. Tail Call Optimization
tailrec fun totalWeight(parts: List<Part>, acc: Double =
0.0): Double {
if (parts.isEmpty()) {
return acc
}
val part = parts.first()
val remainingParts = parts.drop(1)
val currentWeight = acc + part.totalWeight
return when (part) {
is ComplexPart -> totalWeight(remainingParts +
part.parts, currentWeight)
else -> totalWeight(remainingParts, currentWeight)
}
}
public static final double totalWeight(@NotNull List parts, double acc) {
while(true) {
Intrinsics.checkNotNullParameter(parts, "parts");
if (parts.isEmpty()) {
return acc;
}
Part part = (Part)CollectionsKt.first(parts);
List remainingParts = CollectionsKt.drop((Iterable)parts, 1);
double currentWeight = acc + part.getTotalWeight();
if (part instanceof ComplexPart) {
List var10000 = CollectionsKt.plus((Collection)remainingParts,
(Iterable)((ComplexPart)part).getParts());
acc = currentWeight;
parts = var10000;
} else {
acc = currentWeight;
parts = remainingParts;
}
}
}
Variables are
mutable and
algorithm is
iterative
14. Data classes and
Frameworks
Kotlin provides use-site targets
that allow us to specify where
particular annotations have to be
applied. Sometimes we need them
and sometimes we don’t
Why?
15. Working with Data classes
@Table(name = "CAR_PARTS")
@Entity
data class CarPart(
@Id
val id: Long,
@Column
@field:NotNull
@field:Size(min=3, max=20)
val name: String,
val productionDate: Instant,
val expiryDate: Instant,
val barCode: Long,
@field:Min(value = 5)
val cost: BigDecimal
)
@Table(name = "CAR_PARTS")
@Entity
data class CarPart(
@Id
val id: Long,
@Column
@NotNull
@Size(min=3, max=20)
val name: String,
val productionDate: Instant,
val expiryDate: Instant,
val barCode: Long,
@Min(value = 5)
val cost: BigDecimal
)
Doesn’t work Works!
Why
use-site
targets?
16. Working with Data classes
https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets
If you don't specify a use-site target, the target is chosen
according to the @Target annotation of the annotation
being used. If there are multiple applicable targets, the first
applicable target from the following list is used:
● param
● property
● field
17. Working with Data classes
@Target({ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(
validatedBy = {}
)
public @interface Size {
PARAMETER
is
selected
18. Working with Data classes
public final class CarPart {
@Id
private final long id;
@Column
@NotNull
private final String name;
@NotNull
private final Instant productionDate;
@NotNull
private final Instant expiryDate;
private final long barCode;
@NotNull
private final BigDecimal cost;
public final long getId() {
return this.id;
}
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final Instant getProductionDate() {
return this.productionDate;
}
@NotNull
public final Instant getExpiryDate() {
return this.expiryDate;
}
public final long getBarCode() {
return this.barCode;
}
@NotNull
public final BigDecimal getCost() {
return this.cost;
}
public CarPart(long id, @jakarta.validation.constraints.NotNull @Size(min =
3,max = 20) @NotNull String name, @NotNull Instant productionDate, @NotNull
Instant expiryDate, long barCode, @Min(5L) @NotNull BigDecimal cost) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(productionDate, "productionDate");
Intrinsics.checkNotNullParameter(expiryDate, "expiryDate");
Intrinsics.checkNotNullParameter(cost, "cost");
Not where we want
them to be,
but where they are
expected
19. @Table(name = "CAR_PARTS")
@Entity
data class CarPart(
@Id
val id: Long,
@Column
@field:NotNull
@field:Size(min=3, max=20)
val name: String,
val productionDate: Instant,
val expiryDate: Instant,
val barCode: Long,
@field:Min(value = 5)
val cost: BigDecimal
)
Working with Data classes
public final class CarPart {
@Id
private final long id;
@Column
@NotNull
@Size(
min = 3,
max = 20
)
@org.jetbrains.annotations.NotNull
private final String name;
@org.jetbrains.annotations.NotNull
private final Instant productionDate;
@org.jetbrains.annotations.NotNull
private final Instant expiryDate;
private final long barCode;
@Min(5L)
@org.jetbrains.annotations.NotNull
private final BigDecimal cost;
Since @field forces
the target, these
annotations get
applied where they
should
21. Working with Delegates
interface Horn {
fun beep()
}
class CarHorn : Horn {
override fun beep() {
println("beep!")
}
}
class WagonHorn : Horn {
override fun beep() {
println("bwooooooo!")
}
}
annotation class DelegateToWagonHorn
annotation class DelegateToCarHorn
class SoundDelegate(private val initialHorn: Horn) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): Horn {
return initialHorn
}
}
class HornPack {
@delegate:DelegateToWagonHorn
val wagonHorn: Horn by SoundDelegate(CarHorn())
@delegate:DelegateToCarHorn
val carHorn: Horn by SoundDelegate(WagonHorn())
}
Where is this being applied
to?
Horn or SoundDelegate?
22. Working with Delegates
public final class HornPack {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.property1(new
PropertyReference1Impl(HornPack.class, "wagonHorn", "getWagonHorn()Lorg/jesperancinha/talks/carparts/Horn;",
0)), Reflection.property1(new PropertyReference1Impl(HornPack.class, "carHorn",
"getCarHorn()Lorg/jesperancinha/talks/carparts/Horn;", 0))};
@DelegateToWagonHorn
@NotNull
private final SoundDelegate wagonHorn$delegate = new SoundDelegate((Horn)(new CarHorn()));
@DelegateToCarHorn
@NotNull
private final SoundDelegate carHorn$delegate = new SoundDelegate((Horn)(new WagonHorn()));
@NotNull
public final Horn getWagonHorn() {
return this.wagonHorn$delegate.getValue(this, $$delegatedProperties[0]);
}
@NotNull
public final Horn getCarHorn() {
return this.carHorn$delegate.getValue(this, $$delegatedProperties[1]);
}
}
SoundDelegate
No Horn!
23. Working with Delegates
class SanitizedName(var name: String?) {
operator fun getValue(thisRef: Any?,
property: KProperty<*>): String? = name
operator fun setValue(thisRef: Any?,
property: KProperty<*>, v: String?) {
name = v?.trim()
}
}
class PartNameDto {
@get:NotBlank
@get:Size(max = 12)
var name: String? by SanitizedName(null)
override fun toString(): String {
return name ?: "N/A"
}
}
class ImpossiblePartNameDto {
@delegate:NotBlank
@delegate:Size(max = 12)
var name: String? by SanitizedName(null)
override fun toString(): String {
return name ?: "N/A"
}
}
public final class PartNameDto {
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.mutableProperty1(new
MutablePropertyReference1Impl(PartNameDto.class, "name", "getName()Ljava/lang/String;", 0))};
@Nullable
private final SanitizedName name$delegate = new SanitizedName((String)null);
@NotBlank
@Size(
max = 12
)
@Nullable
public final String getName() {
return this.name$delegate.getValue(this, $$delegatedProperties[0]);
}
…
public final class ImpossiblePartNameDto {
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.mutableProperty1(new
MutablePropertyReference1Impl(ImpossiblePartNameDto.class, "name", "getName()Ljava/lang/String;",
0))};
@NotBlank
@Size(
max = 12
)
@Nullable
private final SanitizedName name$delegate = new SanitizedName((String)null);
@Nullable
public final String getName() {
return this.name$delegate.getValue(this, $$delegatedProperties[0]);
}
24. Working with Delegates
@Service
data class DelegationService(
val id: UUID = UUID.randomUUID()
) {
@delegate:LocalDateTimeValidatorConstraint
@get: Past
val currentDate: LocalDateTime by LocalDateTimeDelegate()
}
public class DelegationService {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.property1(new
PropertyReference1Impl(DelegationService.class, "currentDate",
"getCurrentDate()Ljava/time/LocalDateTime;", 0))};
@LocalDateTimeValidatorConstraint
@NotNull
private final LocalDateTimeDelegate currentDate$delegate;
@NotNull
private final UUID id;
@Past
@NotNull
public LocalDateTime getCurrentDate() {
return this.currentDate$delegate.getValue(this, $$delegatedProperties[0]);
}
25. What’s next?
➔ Better understanding of the Kotlin Language.
➔ Don’t fight the Spring Framework or anything else like Quarkus. They are not evil and they are not
magic.
➔ Read the Kotlin documentation and only use Google as a last resort.
➔ Nothing is perfect and Kotlin also falls into that category and recognizing that, allow us to be
better.