-
Notifications
You must be signed in to change notification settings - Fork 2
lessons
Miguel Gamboa edited this page Jun 15, 2022
·
28 revisions
Lessons:
- 07-03-2022 - Lesson 01 - Introduction and Modern VMs, e.g. JVM, Node
- 08-03-2022 - Lesson 02 - Kotlin Type System
- 08-03-2022 - Lesson 03 - Components and Metadata
- 14-03-2022 - Lesson 04 - Programming with Metadata
- 15-03-2022 - Lesson 05 - Programming with Metadata
- 15-03-2022 - Lesson 06 - Logger
- 21-03-2022 - Lesson 07 - Logger and interface Getter
- 22-03-2022 - Lesson 08 - Lab 1 - workout 1 Jsonaif
- 22-03-2022 - Lesson 09 - Lab 2 - workout 1 Jsonaif
- 28-03-2022 - Lesson 10 - Annotations
- 29-03-2022 - Lesson 11 - Lab 3 - workout 1 Jsonaif
- 29-03-2022 - Lesson 12 - Lab 4 - workout 1 Jsonaif
- 04-04-2022 - Lesson 13 - Reflection Exercises
- 05-04-2022 - Lesson 14 - Lab 5 - workout 1 Jsonaif
- 05-04-2022 - Lesson 15 - Lab 6 - workout 1 Jsonaif
- 19-04-2022 - Lesson 16 - Dynamic code E.g. JavaPoet
-
19-04-2022 - Lesson 17 -
LoggerDynamic
- 26-04-2022 - Lesson 14 - Lab 1 - workout 2 Jsonaif
- 26-04-2022 - Lesson 15 - Lab 2 - workout 2 Jsonaif
-
02-05-2022 - Lesson 20 -
LoggerDynamic
- 03-05-2022 - Lesson 21 - Lab 3 - workout 2 Jsonaif
- 03-05-2022 - Lesson 22 - Lab 4 - workout 2 Jsonaif
- 09-05-2022 - Lesson 23 - Types and Objects at runtime
-
10-05-2022 - Lesson 24 - Optimizing
LoggerDynamic
to avoid boxing - 10-05-2022 - Lesson 25 - Lab 5 - workout 2 Jsonaif
-
16-05-2022 - Lesson 26 - Sequences,
Iterable
and Generics - 17-05-2022 - Lesson 27 - Inline functions and Reified type parameters
-
17-05-2022 - Lesson 28 -
Iterable<T>
lazy implementation and Sequenceyield
- 23-05-2022 - Lesson 29 - Exercises with lazy sequences
- 24-05-2022 - Lesson 30 - Lab 1 - workout 3 Jsonaif
- 24-05-2022 - Lesson 31 - Lab 2 - workout 3 Jsonaif
-
30-05-2022 - Lesson 32 - Garbage Collection,
AutoCloseable
andCleaner
- 31-05-2022 - Lesson 33 - Lab 3 - workout 3 Jsonaif
- 31-05-2022 - Lesson 34 - Lab 4 - workout 3 Jsonaif
-
06-06-2022 - Lesson 35 -
Cleaner
- 07-06-2022 - Lesson 36 - Exercícios
- 07-06-2022 - Lesson 37 - Exercícios
- 14-06-2022 - Lesson 38 - Exercícios
- 14-06-2022 - Lesson 39 - Exercícios
- Bibliography and Lecturing methodology: github and slack.
- Tools:
kotlinc
, JDK 17 and gradle. - Program outline in 3 parts:
- Kotlin Type System and Reflection;
- JVM Type System and Metaprogramming;
- Iterators versus Sequences (
yield
). Generics and Reified type parameters.
- Project in 3 parts according to program outline.
- Managed Runtime or Execution Environment.
- Informally virtual machine (VM) or runtime
- Historic evolution since Java to nowadays
- Examples of languages targeting JVM: Java, kotlin, Scala, Clojure.
- Examples of languages targeting Node: JavaScript, TypeScript, Kotlin.
- Java syntax similar to C and C++.
- Distinguish between Programming Language
<versus>
VM - Managed Runtime:
- Software components = Metadata + IR (intermediate representation) (e.g. bytecodes)
- Portability - Compile once and run everywhere.
- Jitter - Just-in-time compiler
- Safety - NO invalid memory accesses
- GC - Garbage Collector.
- ClassLoader and CLASSPATH - dynamic and lazy load.
- CLASSPATH
- Interoperability Java <> Kotlin supported by JVM.
- Using
javap -c -p Point.class
to inspect metadata and bytecodes definition of data classPoint
- Kotlin properties
x
,y
andmodule
map to methodsgetX()
,getY()
andgetModule()
.
- Kotlin properties
- Type System - Set of rules and principles that specify how types are defined and behave.
-
Types are defined through Classes or Interfaces
- On JVM types are defined through Classes.
- Classes have Members.
- Members in JVM may be Fields or Functions (aka as methods).
- Members in Kotlin may be Properties or Functions.
- There are NO Properties at JVM level.
- A Kotlin property may generate:
- Backing field -- accessed with
getfield
bytecode. - Getter, i.e. function
get...
-- called withinvoke...
bytecode. - Setter, i.e. function
set...
(if defined withvar
).
- Backing field -- accessed with
- Kotlin
val
<=>
Javafinal
. -
Any
<=>
JavaObject
, which is the base class of all classes. - Implicit call to base constructor
...: Any()
:- bytecodes:
invokespecial Object.<init>()
- bytecodes:
-
new
in Java andnew
in bytecodes. -
new
is implicit in Kotlin.
-
Component - Reusable software unit, with:
- IR - code in intermediate representation (e.g. bytecodes, IL, other)
- Metadata - auto description
- Ready to use
=>
does not require static compilation. - API
=>
conforms to a public interface. - Indivisible
=>
1 module (file)
-
Software development by Components:
- Reduce Complexity;
- Promote Reuse.
- Developer roles:
- Provider - provides a component with a well-known API (e.g.
Point
) - Client - uses the component from a provider to build an Application, or another component (e.g.
App
).
- Provider - provides a component with a well-known API (e.g.
-
Unmanaged
<versus>
Managed - Static
<versus>
Dynamic link
- Building unmanaged components with static link:
- NOT a unit, but two parts instead: header + obj
- Need of a header file that describes the component content
- Different builds for different architectures
-
Structural modifications (new header)
=>
compilation + link -
Behavioral modifications
=>
link
- Demo with an unmanaged
App
using aPoint
. - Demo with a managed
App
using aPoint
.- Jitter (just-in-time compiler) - compiles IR to native code (e.g. x86, amd64, ppc) at runtime
-
Dynamic link -
Point.class
onApp
compilation + link at runtime. -
Lazy Load -
Point.class
is loaded only when needed
- Reflection object oriented API for metadata
- Kotlin Reflection API (
kotlin.reflect
):KClass
,KCallable
,KFunction
. - Java Reflection API (
java.lang.reflect
):Class
,Member
,Method
andField
.
-
KClass
is representing a type in Kotlin, whereasClass
is representing a type in JVM. -
KClass
--- .java --->
Class
-
Class
---- .kotlin --->
KClass
-
ClassLoader
andURLClassLoader
loadClass(): Class
-
Closeable
interface and Kotlin extensionuse()
- Inspecting functions trough Reflection API
-
KFunction
properties:name
,type
,parameters
andinstanceParameter
. -
KFunction
----->*
KParameter
-
KParameter
propertykind
:INSTANCE
versusEXTENSION_RECEIVER
-
KParameter
propertyisOptional
-
KClass
::createInstance()
-
KFunction
::call()
- Collections extensions:
map
,filter
,forEach
- Implementing a
ZipInputStream
extensioniterable()
with an explicitIterator
implementation.
- Distinguishing the role of
App
domain--->
Logger
utility. - Unit tests.
- Code coverage.
- Decouple the output into a
Printer
interface. - Implement
PrinterConsole
andPrinterStringBuffer
. class Logger(val out: Printer = PrinterConsole())
- Refactor Logger to avoid
if
/else
validation onlog()
execution. - Maintain getters on
MutableMap<KClass<*>, List<Getter>>
interface Getter {fun readAndPrint(target: Any) }
- Distinct implementations:
createGetterProperties
,createGetterFunctions
, ...
fun KClass<*>.isSubclassOf(base: KClass<*>): Boolean
fun KClass<*>.isSuperclassOf(derived: KClass<*>): Boolean
- You have to develop a
ComparerOrder(klass: KClass<Any>)
that implementsComparator
and it is able to compare instances of type represented byklass
, according to the properties which are both:Comparable
and annotated withComparison
. Notice that you should compare respecting the order sepcified in annotation. Example:
class Student (
@Comparison(2) val nr:Int,
val name: String,
@Comparison(1) val nationality: String,
) |
val s1 = Student(12000, "Ana", "pt")
val s2 = Student(14000, "Ana", "pt")
val s3 = Student(11000, "Ana", "en")
val cmp = ComparerOrder(Student::class)
assertTrue { cmp.compare(s1, s2) < 0 } // same nationality and 12000 is < 14000
assertTrue { cmp.compare(s2, s3) > 0 } // “pt” is > “en” |
- You have to develop a
Comparer(klass: KClass<Any>)
that implementsComparator
and it is able to compare instances of type represented byklass
, according to the properties which are:Comparable
OR annotated with aComparison
that specifies aComparator
for that property, according to the following example:
class Person(
val id: Int,
val name: String,
@Comparison(cmp = AddressByRoad::class) val address: Address,
@Comparison(cmp = AccountByBalance::class) val account: Account) {
}
class AccountByBalance : Comparator<Account>{
override fun compare(o1: Account, o2: Account): Int {
return o1.balance.compareTo(o2.balance);
}
}
class AddressByRoad : Comparator<Address> {
override fun compare(o1: Address, o2: Address): Int {
return o1.road.compareTo(o2.road)
}
} |
val p1 = Person(11000, "Ana", Address("Rua Amarela", 24), Account("FD3R", 9900))
val p2 = Person(11000, "Ana", Address("Rua Rosa", 24), Account("8YH5", 9900))
val p3 = Person(11000, "Ana", Address("Rua Rosa", 24), Account("JK2E", 100))
val p4 = Person(11000, "Ana", Address("Rua Rosa", 97), Account("BFR5", 100))
val p5 = Person(17000, "Ana", Address("Rua Rosa", 97), Account("BFR5", 100))
val cmp = Comparer<Person>(Person::class)
assertTrue { cmp.compare(p1, p2) < 0 } // Rua Amarela is < Rua Rosa
assertTrue { cmp.compare(p2, p3) > 0 } // 9900 is > 100
assertEquals(0, cmp.compare(p3, p4)) // All properties are equal
assertTrue { cmp.compare(p4, p5) < 0 } // 11000 is < 17000 |
- JavaPoet API
- Refactoring Logger with new:
-
GetterProperty
andGetterFunction
implementations of interfaceGetter
-
AbstactLogger
base class ofLoggerReflect
andLoggerDynamic
-
- Skeleton for
buildGetterProperty(klass: KClass<*>, prop: KProperty<*>)
that builds a custom implementation ofGetter
for given domainklass
and propertyprop
.
- Finishing
buildGetterProperty()
andbuildGetterFunc()
. - Finishing
LoggerDynamic
and unit tests.
- Benchmark - assess the relative performance
- Benchmark != Unit Tests
- Minimize side effects:
- Garbage Collector may degrade performance => Run several iterations and discard most divergent results.
- Optimizations may improve performance => Include warm-up.
- Avoid IO
- Avoid extra tools such as IDE (e.g. InteliJ), gradle, or other => run directly on VM (e.g.
java
)
- 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
-
Benchmark Mode Cnt Score Error Units
benchLogSavingsAccountViaDynamic thrpt 4 8511,015 254,220 ops/ms
benchLogSavingsAccountViaReflection thrpt 4 4823,931 160,847 ops/ms
- Analyze bytecodes of
Getter
regardingObject v = ((SavingsAccount) target).getBalance();
- Equivalent to:
long bal = ((SavingsAccount) target).getBalance();
Object v = bal; // IMPLICIT call to Long.valueOf(long)
- Primitive
<versus>
Reference types -
int
,long
,double
, ...<versus>
Integer
,Long
,Double
, ... - Local values in Stack
<versus>
Values in Heap. - 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 .
- 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);
- 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
- Object's layout and header (containing type information):
-
header is useful for operations such as
obj::class
(<=>
obj.getClass()
in Java)
-
header is useful for operations such as
-
Reference Types are instantiated in bytecodes 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 bytecodes:
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>"
-
Casting between reference types:
- upcasting in bytecodes it is only copying references, e.g.
aload_0
andastore_1
- downcasting includes
checkcast
bytecode
- upcasting in bytecodes it is only copying references, e.g.
-
checkcast
checks for compatibility (i.e. same type or subtype). -
checkcast
throwsClassCastException
on unsuccess, otherwise leaves the object's reference on top of the stack.
- 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. - E.g. in .Net the IL language (
<=>
bytecodes) there are opcodes forbox
andunbox
.
- 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 |
---|---|---|
Primitive |
int , long , double , ... |
Int , Long , Double , ... |
Reference |
Integer , Long , Double , ... |
Int? , Long? , Double? , ... |
Object v = ((SavingsAccount) target).getBalance(); // boxing: Integer.valueOf(...)
out.print("balance = " + v + ")");
- 1st approach - replace
Object
byint
int v = ((SavingsAccount) target).getBalance();
out.print("balance = " + v + ")");
- Yet, the second statement still requires boxing
v
. - 2nd approach - overload
print(<primitive>)
for each primitive type:
int v = ((SavingsAccount) target).getBalance();
out.print("balance = "); // invoke Printer::print(String)
out.print(v); // invoke Printer::print(int)
out.print(")"); // invoke Printer::print(String)
- Speedup from 2x to 3x on
LoggerDynamic
for domain objects with primitive fields:
Benchmark Mode Cnt Score Error Units
benchLogSavingsAccountViaDynamic thrpt 4 15069,625 7535,855 ops/ms
benchLogSavingsAccountViaReflection thrpt 4 4705,736 753,258 ops/ms
-
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."
- e.g. method chaining:
sudents.filter(...).map(...).distinct(...).count()
- e.g. nested function:
count(distinct( map (... filter (...))))
- 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.
-
- Similarities to SQL for relational data, e.g.
SELECT COUNT (DISTINCT ...) FROM ... WHERE ...
- Distinguishing between:
- General-purpose language, e.g. Kotlin, Java C#,
- Domain-specific languages, e.g. SQL, Linq, Jetpack Compose,
-
Query Example:
From a students listing select the first surname starting with letter A of a student with number greater than 47000
. - Divide and reorder requirements:
-
From a students listing...
- data source -
... of a student with number greater than 47000
- - intermediate operation -
... surname ...
- intermediate operation -
... starting with letter A...
- - intermediate operation -
... select the first...
- terminal operation
-
- One operation for each requirement:
File(…).readLines().parseCsv() // List<Map<String, String>>
.convertToStudent() // List<Student>
.whereNrGreaterThan(47000) // List<Student>
.convertToSurname() // List<String>
.whereStartsWith(“A”) // List<String>
.iterator().next() // Select first occurence
-
Approach 1 problems:
- Eager processing overhead => traverses all elements regardless the number of selected items (i.e. first occurrence in this example).
- GC overhead => each intermediate operation stores all elements in an auxiliary
List
that is candidate for GC after operation's completion. - Repeated code between
convert...
andwhere...
- Mixing Domain and Utility features
-
Approach 2 - implement generic methods
convert
andwhere
:fun <T, R> Iterable<T>.convert(transform: (T) -> R): Iterable<R>
fun <T> Iterable<T>.where(predicate: (T) -> Boolean): Iterable<T>
- Eager implementation of
select
similar tomapTo
of Collections Extensions:
fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}
- Eager implementation of
filter
similar tofilterTo
of Collections Extensions:
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this)
if (predicate(element))
destination.add(element)
return destination
}
-
Approach 3 - Implement a generic auxiliary
toObject()
:fun <T : Any> Map<String, String>.toObject(destination: KClass<T>) : T
- that replaces the use of
fun Map<String, String>.toStudent() : Student
-
Type Erasure:
- Replace all type parameters in generic types with their bounds or
Object
- Insert type casts (i.e. bytecode
checkcast
) if necessary to preserve type safety.
- Replace all type parameters in generic types with their bounds or
-
Type Erasure and the need of parameter
destination: KClass<T>
- Inline functions and Reified type parameters
to allow the use of
T::class
-
Approach 4 -
Iterable<T>
lazy implementation ofconvertLazy
andfilterLazy
- Comparing the number of calls to
transform
between an eager and lazy pipeline. - Approach 5 - sequence builder that lazily yields values one by one.
fun <T, R> Sequence<T>.convert(transform: (T) -> R) = sequence {
for (item in this@convert) {
yield(transform(item))
}
}
fun <T> Sequence<T>.concat(other: Sequence<T>) = Sequence<T>
fun <T : Any?> Sequence<T>.collapse() = Sequence<T>
fun <T> Sequence<T>.window(size: Int) = Sequence<Sequence<T>>
- 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.
- 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
- Demo
FileStream
keeping a handle to a native resource. - Avoid using:
- The try-with-resources Statement
- A resource is an object that must be closed after use.
- Kotlin
use{}
extension
-
Cleaner
- manage cleaning actions. - 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 theCleaner
.
-
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 theCleaner
.