Generics

Using generics.

How we would use data type specifications in Java and their benefits:

public class JavaGenerics {
    public static void main(String[] args) {

        List<String> list = new ArrayList<>(); // By giving an indication of data type we have improved error-checking, need less casting and code is clearer

        list.add("Hello");
//        list.add(new BigDecimal(10.5)) // Error-checking picks up on the incompatibility
        list.get(0).toUpperCase(); // IDE suggests functions related to data type

        List list1 = new ArrayList<>(); // However, we can still create a Collection without specifying type

    }
}

The same in Kotlin, introducing Generics:

// Use generics to give the compiler a hint as to what type of objects you are dealing with, often related to Collections
// Allows compiler to do some error-checking at compile time
fun main() {

    val list: MutableList<String> // Kotlin insists on knowing the data type
    list = mutableListOf("Hello")
    list.add("another string")
    printCollection(list)
    list[0].toUpperCase() // We can use the relevant functions

    println("======")

    val bdList = mutableListOf(BigDecimal(-33.45), BigDecimal(3503.99), BigDecimal(0.329))
//    printCollection(bdList) // Won't allow printCollection because it expects String
    printGenericCollection(bdList) // Works because Generics are used

    println("======")

    bdList.printCollectionExtension() // Same result using extension function - IDE suggests

// At runtime the application has none of the Generic information - type erasure. It is simply a syntactical feature e.g. it cannot run 'instanceOf' on a Generic since that information does not exist at runtime.
// Also true of Kotlin since it uses the JVM. How do we use the 'is' operator (equivalent of instanceOf)?
    if (list is List<String>){} // This is allowed! How come, since Generics are lost at runtime? Suspect the function is handled at compile-time by Kotlin
    val listAny: Any = listOf("str1", "str2") // To check this theory...
//    if (listAny is List<String>) {} // This is invalid as the statement cannot be evaluated at compile-time
//    if (listAny is List) // We can't even do this since Kotlin expects a data type
    if (listAny is List<*>) {// So we do this instead - star projection syntax, sort of like wildcard
        println("So let's assume this List contains strings") // If it isn't a List of non-Strings this
        val strList = listAny as List<String> // Casting. This is how we use 'as' in conjunction with this. If it isn't a List of Strings it will throw a ClassCastException. Compiler will warn
        println(strList[1].replace("str", "string"))
    }
}


fun printCollection(collection: List<String>) {
    for (item in collection) {
        println(item)
    }
}

fun <T> printGenericCollection(collection: List<T>) { // The <T> after fun is the type parameter declaration and is necessary
    for (item in collection) {
        println(item)
    }
}

// Extension function example:
fun <T> List<T>.printCollectionExtension() {
    for (item in this) {
        println(item)
    }
}