lessons - isel-leic-ave/2025-lae-42d-44d GitHub Wiki
Lessons:
- 17-02-2025 - Lesson 01 - Chapter 1 - Introduction and Modern VMs; Class Loader
- 21-02-2025 - Lesson 02 - Chapter 3.1 - Reflect Intro in Kotlin
- 24-02-2025 - Lesson 03 - Chapter 3.2 - Reflect in Java; e.g. Logger
- 24-02-2025 - Lesson 04 - Chapter 3.3 and 3.4 - Reflect; Annotations; e.g. Object Mapper
- 28-02-2025 - Lesson 05 - Trab 0 - jsonoy - Lab 01
- 28-02-2025 - Lesson 06 - Trab 0 - jsonoy - Lab 02
- 28-02-2025 - Lesson 07 - Trab 0 - jsonoy - Lab 03
- 10-03-2025 - Lesson 08 - Chapter 4 - Performance testing and Microbenchmarking
- 10-03-2025 - Lesson 09 - Chapter 3.3 - Minimizing Reflection Usage
- 14-03-2025 - Lesson 10 - Chapter 3.5 - Reflection of generic types
- 18-03-2025 - Lesson 11 - Enhance mapFrom(); JMH framework;
- 18-03-2025 - Lesson 12 - Chapter 2.1 - Java Type System
- 21-03-2025 - Lesson 13 - Chapter 2.2, 2.3, 2.5 - Abstract and Base Types; Anonymous classes; Function Types
- 24-03-2025 - Lesson 14 - Object declarations (Chap 2.4.2); Boxing and Unboxing (Chap 2.1.4)
- 28-03-2025 - Lesson 15 - Trab 1 - jdbcRepo - Lab 04
- 28-03-2025 - Lesson 16 - Trab 1 - jdbcRepo - Lab 05
- 31-03-2025 - Lesson 17 - 2.2: Abstract and Base Types (Chap 2.2);
- 04-04-2025 - Lesson 18 - Trab 1 - jdbcRepo - Lab 06
- 04-04-2025 - Lesson 19 - Trab 1 - jdbcRepo - Lab 07
- 07-04-2023 - Lesson 20 - Bytecode
- 11-04-2023 - Lesson 21 - Metaprogramming; Class-File API;
- 11-04-2025 - Lesson 22 - Trab 2 - jdbcRepo - Lab 08
- 14-04-2023 - Lesson 23 - NaiveMapper Dynamic
- 14-04-2025 - Lesson 24 - Trab 2 - jdbcRepo - Lab 09
- 28-04-2025 - BLACKOUT
- 02-05-2025 - Lesson 25 - Trab 2 - jdbcRepo - Lab 09
- 05-05-2025 - Lesson 26 - Trab 2 - jdbcRepo - Lab 10
- 05-05-2025 - Lesson 27 - Trab 2 - jdbcRepo - Lab 11
- 09-05-2025 - Lesson 28 - Sequences and Lazy processing
-
09-05-2025 - Lesson 29 - Generators and
yield
operator - 12-05-2025 - Lesson 30 - Exercises Sequences and Generators
- 16-05-2025 - Lesson 31 - Exercises Sequences and Generators
- 19-05-2025 - Lesson 32 - Deconstructing yield and Suspend Functions
- 19-05-2025 - Lesson 33 - Trab 3 - jdbcRepo - Lab 12
- 22-05-2025 - Lesson 34 - Garbage Collector
- Bibliography and Lecturing methodology: github and slack.
- Tools:
javac
,javap
,kotlinc
, JDK 21 andgradle
. - Program outline in 3 parts:
- Java Type System and Reflection;
- Metaprogramming and Performance;
- Lazy processing.
- Project in 3 parts according to program outline.
- Managed Runtime or Execution Environment or informally virtual machine (VM) or runtime.
-
Execution Environment includes:
- Compiler,
- Programming languages,
- Standard libraries,
- Dependency manager (e.g. gradle, maven)
- Central Repository (e.g. Maven Central Repository, Nuget, NPM, etc)
- Examples of languages targeting JVM: Java, kotlin, Scala, Clojure.
- Examples of languages targeting Node: JavaScript, TypeScript, Kotlin.
- JVM runs
.class
files with bytecode - JVM translates bytecode to machine code (e.g. IA-32, AMD64, etc depending of the CPU)
- In Javascript ecosystem modules are deployed in source code i.e. Javascript.
- Distinguish between Programming Language
<versus>
VM
-
.class
= Unit of SW distribution in JVM - One file
.class
for each class definition - Class Loader
- Delayed Loading
- Just-in-time Compiler
- Reflection object oriented API for metadata
-
Reflection
--->
metadata--->
Type System - Type System: types have members
-
Kotlin Reflection API:
KClass
----->*
KCallable
- An instance of
KClass
may represent a type in Kotlin. - An instance of
KCallable
may represent a member in Kotlin.
- An instance of
-
KCallable
base type ofKFunction
andKProperty
-
KProperty
andKMutableProperty
-
KCallable
----->*
KParameter
-
KFunction
properties:name
,type
,parameters
andinstanceParameter
. -
KParameter
propertykind
:INSTANCE
versusEXTENSION_RECEIVER
-
KParameter
propertyisOptional
-
KClass
::createInstance()
-
KFunction
::call()
- Java Reflect API;
- Simple Logger both using Kotlin and Java Reflect API;
-
NaiveMapper
, takes inspiration from libraries likeAutoMapper
orMapStruct
:- Simplify the process of mapping data between objects of different types by copying values from properties of one object to corresponding properties of another object.
- Offers an extension function like
Any.mapTo(dest: KClass<*>): Any
- 1st version - through mutable properties (i.e.
KMutableProperty
):- The destination type must have a parameterless constructor
- The source and destination properties share the same name and type
- The destination properties are mutable
- 2nd version - Match properties with different name.
- Annotations in the JVM are a form of metadata that can be added to Java classes, methods, fields, and other program elements.
- Annotations are strongly typed
- Each annotation inherits from
java.lang.annotation.Annotation
- E.g. JUnit annotation
@Test
corresponds to the following type:
public interface org.junit.Test extends java.lang.annotation.Annotation{...}
- Kotlin Reflect API on annotations:
annotations: List<Annotation>
findAnnotation<T>(): T
hasAnnotation<T>(): Bool
- When a Kotlin member generates multiple Java members, there are multiple potential locations.
- Use site target to explicitly specify the destination location within the metadata:
- e.g.
@property:MapProp("from") val country: String
- e.g.
- Specify the allowed elements with the
@Target
annotation. - Enhance
NaiveMapper
to map properties to parameters with different name through the annotation@Match
- Benchmark - assess the relative performance
- Benchmark != Unit Tests
- A naif approach - direct measurement, e.g.
measureTimeMillis { System.out.log(Rectangle(...)) }.also { dur -> println("Logging rectangle takes $dur ms"); }
- Some Problems:
- IDE (e.g. InteliJ) may induce other overheads.
- Mixing domain instantiation (i.e.
Rectangle
) with operation executionlog()
. - First execution includes Jitter overhead and misses optimizations.
- IO may be orders of magnitude slower than
log
operation itself - Milliseconds could not be accurate enough.
-
System.currentTimeMillis()
includes a System call with implicit overhead. - Garbage Collector may degrade performance
- Absolute performance analysis Leads to bias
- Minimize side effects:
- Avoid extra tools such as IDE (e.g. InteliJ), gradle, or other => run directly on VM (e.g.
java
) - Remove domain instantiation from operation measurement
- Include warm-up => Optimizations may improve performance
- Avoid IO => Mocking IO
- Measure the total execution of several iterations rather than several measurements of single executions.
- same as 6.
- Run several iterations and discard most divergent results.
- Baseline => Use a reference comparison to estimate how much we can improve performance.
- Avoid extra tools such as IDE (e.g. InteliJ), gradle, or other => run directly on VM (e.g.
- Comparing performance between a:
- Reflection-based
mem.log(Rectangle(...))
- Baseline ad-hoc
rect.log(mem)
- Reflection-based
- Reflection-based approach experiences a slowdown of around 5x:
Bench Log via Reflect takes ~300 nanos
Bench Log via baseline takes ~60 nanos
- Comparing performance between a:
- Reflection-based
dto.mapTo(Person::class)
- Baseline ad-hoc
dto.toPerson()
- Reflection-based
- Reflection-based approach experiences a slowdown of around 140x:
Bench Reflect mapTo(Person::class) takes ~700 nanos
Bench baseline toPerson() takes ~5 nanos
- New
NaiveMapper
that stores information about matching KProperty instances. - Approach: Look once for matching properties (1.) and use many times copying values (2.):
val props: Map<KProperty<*>, KMutableProperty<*>?>
props.forEach({ (from, to) -> to?.setter?.call(target, from.call(src)) })
- Comparing performance improves slowdown of Reflection-based to 34x:
Bench Reflect mapTo(Person::class) takes ~700 nanos
Bench Reflect NaiveMapper.mapFrom(dto) takes ~170 nanos
Bench baseline toPerson() takes ~5 nanos
- Enhance
NaiveMapper
:- The destination properties can be immutable and the destination type does not require a parameterless constructor
- The source and destination properties may have different types when they are of domain class types.
-
KCallable
:-
call(vargars)
- arguments should be passed in the same order as the formal parameters of the function. -
callBy(Map<KParameter, Any?>)
- the positional order of the arguments is determined by the information of eachKParameter
.
-
- To directly reference a
KType
, we use thetypeOf
function:- e.g.
func.returnType != typeOf<Unit>()
- e.g.
-
KType
holds information about nullability and type arguments. -
KType
properties:isMarkedNullable
,arguments
, andclassifier
.-
arguments
provide information about the type arguments (i.e.List<KType>
) -
classifier
provides a reference to the associated class (i.e.KClassifier
).-
KClassifier
is the base type ofKClass
-
-
- Enhance
mapFrom()
to suppress the overhead of type checking:- i.e.
if (srcProp.returnType != ctorParam.type) { ... }
- i.e.
- Check types on mapper initialization and manage 2 different versions of a property mapper functions (i.e.
(Any?) -> Any?)
):- If same type -
{ propValue -> propValue }
(identity function) - For different types -
{ propValue -> propertyMapper.mapFrom(propValue) }
- If same type -
- Store the property mapper function (i.e.
(Any?) -> Any?)
) in theprops
data structure:props: List<PropInfo>
class PropInfo(val srcProp: KProperty<*>, val ctorProp: KParameter, val mapPropValue: (Any?) -> Any?)
- Enhanced
mapFrom
:
fun mapFrom(src: T): R = props
.associate { (srcProp, ctorParam, mapPropValue) ->
val propValue = srcProp.call(src)
ctorParam to mapPropValue(propValue)
}
.let { params: Map<KParameter, Any?> ->
constructor.callBy(params)
}
- JMH - Java Microbenchmark Harness.
- Benchmark tests annotated with
@Benchmark
. - JMH Gradle Plugin
gradlew jmhJar
-
java -jar <path to JAR> -f 1 -wi 4 -i 4 -w 2 -r 2 -tu ms
:-
-f
- forks -
-wi
- warm-up iterations -
-i
- iterations -
-w
- each warm-up iteration duration -
-r
- each iteration duration -
-tu
- time unit
-
- Type System - Set of rules and principles that specify how types are defined and behave.
- Two kinds of types: Primitive and Reference types.
- Classes have Members
- Members may be: Fields or Methods.
- There are NO properties in Java Type System.
- Using
javap -p Rectangle.class
to inspect metadata - The fully qualified name of a class includes its package name.
-
Constructor is a method with the name
<init>
returning void.
- Member access syntax:
Receiver.Member
. - The Receiver is the target of the member access and it is a Type (for static members) or an Object (for non-static/instance members).
- JOL - Java Object Layout:
java -cp .;jol-cli-0.17-full.jar org.openjdk.jol.Main estimates <classqualifiedname>
- (linux replace
;
by:
on-cp
(classpath))
- Object header = mark word (used for hash, locks, GC, etc) + class word (class-specific metadata).
- Fields alignment, reorder and padding gap.
- Nested Types
- Abstract and Base Types
- Anonymous classes
- Function Types
- Singleton design pattern
-
object
keyword:- private constructor
- Singleton instance in static INSTANCE field;
- Static initializer: initializes static fields (i.e.
<cinit>()
) - companion object - specific type of object declaration associated with its owner class.
- Boxing and Unboxing.
- Categories of Types:
-
Value Types (e.g. Primitive types, e.g. Java
int
,long
,double
,...) -
Reference Types (e.g.
class
,interface
,...)
-
Value Types (e.g. Primitive types, e.g. Java
- Values <versus> Objects (in Heap);
- Primitive
<versus>
Reference types -
int
,long
,double
, ...<versus>
Integer
,Long
,Double
, ... - Autoboxing is the automatic conversion that the Java compiler makes between the primitive types and their corresponding object wrapper classes.
-
unboxing - converting an object of a wrapper type (e.g.
Integer
) to its corresponding primitive (int
) value is called . - Primitive
<->
Reference: boxing or unboxing:- There is no specific JVM bytecode for these conversions.
- Supported in JVM through auxiliary functions (i.e.
valueOf()
and<type>Value()
) of Wrapper classes.
- Primitive Type
->
Reference: boxing through<Wrapper Type>.valueOf(primitive)
- e.g.
fun boxing(nr: Int) : Any { return nr }
- e.g.
- Reference Type
->
Primitive: unboxing through<Wrapper>.<primitive>Value()
- e.g.
fun unboxing(nr: Int?) : Int { return nr ?: throw Exception("Null not supported for primitive Int!") }
- e.g.
Types | Java | Kotlin |
---|---|---|
Value |
int , long , double , ... |
Int , Long , Double , ... |
Reference |
Integer , Long , Double , ... |
Int? , Long? , Double? , ... |
- Names collision and Member access ambiguity
- Methods call resolution: Fields, Methods, and Virtual Methods.
- Override.
Methods | Kotlin | Java |
---|---|---|
Static Dispatch i.e. type of the receiver |
- |
final static
|
Dynamic Dispatch i.e. type of the object |
open abstract
|
abstract |
Homework
- Consider the following definition of types A, B, C, and I. Given the use of those types in
main
, answer the following questions:- Predict the output of the execution of the
main
function. - What are the differences in resulting output if we remove the
final
keyword fromvirtualFoo
in class B? - Keeping the definition of types A, B, C, and I as it is, what would be the result of adding the following implementation in class C:
public void virtualFoo() { out.println("C"); }
- Provide an equivalent implementation in Kotlin for types A, B, C, and I, with only the method
virtualFoo
to achieve the same result as in question 1. Do not include the static methodfoo
.
- Predict the output of the execution of the
interface I { void virtualFoo(); }
class A {
public static void foo() { out.println("A"); }
public void virtualFoo(){ out.println("A"); }
}
class B extends A implements I {
public static void foo() { out.println("B"); }
public final void virtualFoo(){ out.println("B"); }
}
class C extends B {
public static void foo(){ out.println("C"); }
} |
public static void main(String[] args) {
final C c = new C();
final A a = c;
final B b = c;
final I i = c;
a.foo();
a.virtualFoo();
b.foo();
b.virtualFoo();
c.foo();
c.virtualFoo();
i.virtualFoo();
} |
- Evaluations Stack
- Local variables and Arguments
- Constant Pool
- 16-bit index into the constant pool
- Load and store opcodes
- Shortcut opcode forms
- Arithmetic
- Execution Flow
-
Reference Types are instantiated in bytecode with:
-
new
- Allocates storage on Heap, initializes space and the object's header, and returns the reference to newbie object. -
invokespecial
- Call to class<init>
method (corresponding to constructor).
-
- Instantiating a refence type, e.g.
Student(765134, "Ze Manel")
may produce in bytecode:
new #8 // class Student
dup // duplicates the value on top of the stack
... // One load (push) for each parameter of <init> (constructor)
invokespecial #14 // Method Student."<init>"
-
invokestatic
- nothis
required -
invokespecial
- static dispatch (i.e. non polymorphic) -
invokevirtual
- dynamic dispatch (i.e. polymorphic) -
invokeinterface
- dynamic dispatch (i.e. polymorphic) for interface methods
Homework
- Run
java Foo
in thelesson20-TPC
folder and observe the output. - Write the Java equivalent of the
bar
function defined inFoo.class
fromlesson20-TPC
. - Test your implementation with the arguments
12
and19
to ensure it produces the same output as in step 1.
---
config:
theme: mc
look: classic
layout: dagre
---
classDiagram
direction LR
class ClassFile {
of() ClassFile
build(ClassDesc, Consumer~ClassBuilder~) byte[]
}
class ClassBuilder {
withFlags(int)
withField(String, ClassDesc, int, Consumer~FieldBuilder~)
withMethod(String, MethodTypeDesc, int, Consumer~MethodBuilder~)
}
class MethodBuilder {
withCode(Consumer~CodeBuilder~)
}
ClassFile ..> ClassBuilder
ClassBuilder ..> FieldBuilder
ClassBuilder ..> MethodBuilder
- New Dynamic Mapper that suppresses Reflect on:
- Getting properties from source object
- Instantiating the target class
- Common base interface for
NaiveMapper
and Dynamic Mappers:interface Mapper<T> { fun mapFrom(source: Any): T }
-
NOTE:
- There is a single
NaiveMapper
class using Reflect - There are different Mapper classes dynamically generated for each pair
srcKlass to destKlass
- There is a single
- Each Dynamic Mapper has a different implementation of
mapFrom
.
Java Reflect <interop>
Kotlin Reflect:
- Annotation
@JvmOverloads
-- Instructs the Kotlin compiler to generate overloads for a function that substitute default parameter values. -
javac -parameters
- Generates metadata for reflection on method parameters.- Gradle :
tasks.compileKotlin { kotlinOptions { javaParameters = true } }
- Gradle :
- Collection Pipeline - "organize some computation as a sequence of operations which compose by taking a collection as output of one operation and feeding it into the next."
- Advantages:
- Composability
- Expressivity/Readability
- Extensibility
- Alternative pipelines idioms:
- e.g. method chaining:
students.filter(...).map(...).distinct(...).count()
- e.g. nested function:
(count(remove-duplicates(mapcar #'map-function(remove-if-not ...))))
- e.g. method chaining:
- Collection pipeline:
-
Data Source
-->
Intermediate Operation*
-->
Terminal Operation
- May have many intermediate operations.
- After the terminal operation we cannot chain any intermediate operation.
-
- May distinguish by:
- Idiom of combining functions: method chain versus nested functions
- API methods names: e.g.
filter
,drop
,reduce
, etc versuswhere
,skip
,fold
, etc (in Dart) -
Eager versus Lazy, e.g. JavaScript Array methods versus Java
java.util.stream
-
Query Example:
Distinct weather descriptions in rainy days
.
weatherData
.map { it.weatherDesc }
.filter { it.lowercase().contains("rain") }
.distinct()
weatherData
.filter { it.weatherDesc.lowercase().contains("rain") }
.map { it.weatherDesc }
.distinct()
- The order of intermediate operations in the pipeline
=>
may impact the total number of iterations. - Custom implementation of an equivalent
eagerMap
andeagerFilter
.
- Kotlin 2 distinct APIs for collections pipeline:
Iterable
versusSequence
-
Iterable
and Collections are eager with horizontal processing -
Sequence
should be lazy with vertical processing and can be infinite. -
Marble diagrams:
- Time/Data is horizontally from left to right.
- Operations flow vertically, representing the sequence of operations applied to the data stream
- Implementing
lazyMap
equivalent to the lazy version ofmap
- Explicit implementation of interface
Iterator
forlazyMap
- Homework:
- Implement
lazyDistinct
.- Explicit implementation of interface
Iterator
forlazyDistinct
- Explicit implementation of interface
- Implement
-
Generator:
- Like a function, but instead of returning a single value, it produces a sequence of values.
- Uses the
yield
keyword to return the next item in the sequence to the caller. - Computation is suspended at
yield
and resumed when the caller requests the next item (next()
).
- A generator can remain suspended indefinitely without being resumed from its caller.
-
sequence
- Builds a Sequence lazily yielding values one by one. -
suspend fun yield(value: T)
- Yields a value to theIterator
being built and suspends until the next value is requested.
Homework -- Exercises with lazy sequences:
- Implementing alternative
suspendDistinct
in 4 lines using the yield. -
fun <T> Sequence<T>.lazyConcat(other: Sequence<T>): Sequence<T>
with explicit implementation of interfaceIterator
-
fun <T> Sequence<T>.suspendConcat(other: Sequence<T>): Sequence<T>
usingyield
-
fun <T : Any?> Sequence<T>.collapse(): Sequence<T>
- merges series of adjacent elements
- Exercises Sequences and Generators
- Write an extension function
suspZip
forSequence<T>
that combines the receiver sequence with another sequence (other
) into a newSequence<V>
. - The combination must be done element-wise using the provided
transform
function.
fun <T, R, V> Sequence<T>.suspZip(
other: Sequence<R>,
transform: (a: T, b: R) -> V
): Sequence<V>
- The resulting sequence must be lazy (like
zip
) and should terminate when either input sequence runs out of elements. - The function should not materialize the sequences in memory (i.e. do not convert to lists/arrays).
- This function is not suspendable, despite the name
val numbers = sequenceOf(1, 2, 3)
val letters = sequenceOf("a", "b", "c", "d")
val zipped = numbers.suspZip(letters) { num, letter -> "$num-$letter" }
println(zipped.toList()) // Output: [1-a, 2-b, 3-c]
Continuation<T>
resumeWith(result: Result<Unit>)
- Suspension points and State Machine
- Example cast
fetchSuspend(String)
infetchCps(url: String, onComplete: Continuation<String>): Any
- Example cast
fetchCps(url: String, onComplete: Continuation<String>): Any
to:suspend (String) -> String
- Local values in Stack
<versus>
Objects in Heap. - Local variables managed on stack <versus> Objects managed on Heap by Garbage Collector
- Stack frame on function's execution => cleaned on function completion, including local variables.
- Unreachable objects are candidates for Garbage Collector
- Garbage Collection - "process of looking at heap memory, identifying which objects are in use and which are not, and deleting the unused objects."
- Unused object, or unreferenced object.
- In use object, or a referenced object:
- "some part of your program still maintains a pointer to that object."
- Referenced by a root or belongs to a graph with a root reference.
-
root reference:
- Local variables - stored in the stack of a thread.
- Static variables - belong to the class type stored in permanent generation.
- GC basic process: 1. Marking and 2. Deletion.
-
Deletion approaches:
- Normal Deletion => holds a list to free spaces. !! memory allocation slower !!
- Deletion with Compacting => move referenced objects together ++ makes new memory allocation faster !!! longer garbage collection time !!!
-
Generational Garbage Collection:
- Most objects are short lived => GC is more effective in younger generations.
- Enhance the performance
-
JVM Generations:
- Young Generation is where all new objects are allocated and aged.
- Old Generation is used to store long surviving objects.
- Permanent generation contains metadata, classes and methods.
- "Stop the World" events:
- minor garbage collection - collects the young generation.
- major garbage collection - collects the old generation.
- Young Generation process: Eden, Survivor Space 0 and Survivor Space 1.
- The try-with-resources Statement
- A resource is an object that must be closed after use.
- Kotlin
use{}
extension
- Finalization - used to reclaim native resources associated with an object.
- Lifetime of a finalizable object:
- created
- unreachable
- added to finalization queue
- finalized (closed)
- GC deleted
- Finalization can delay the GC
- Avoid Memory-Retention Problems When Subclassing
-
Cleaner
- manage cleaning actions.-
public Cleanable register(Object obj, Runnable action)
- Registers an object and a cleaning action, i.e.Runnable
-
Cleanable
has a single methodclean()
that runs the cleaning action and unregisters the cleanable.
-
- Explicitly invoke the
clean()
method when the object is closed or no longer needed. - If the close() method is not called, the cleaning action is called by the Cleaner.