Last Updated: 2021-05-30

¿Qué es Kotlin?

Kotlin es un lenguaje de programación de alto nivel y código abierto, el cual es estática y fuertemente tipado; además, brinda soporte multiplataforma por medio de diferentes compiladores. Uno de sus principales usos está en las aplicaciones de Android, aunque puede ser utilizado para muchas otras cosas, como el desarrollo de aplicaciones server-side, o para ser integrado con JavaScript.

Kotlin es un lenguaje funcional con soporte a Programación Orientada a Objetos (POO), el cual nació en 2011 y permite desarrollar código robusto, legible y con soporte a Java.

¿Qué construirá?

Al final de este tutorial usted tendrá:

¿Qué aprenderá?

Al desarrollar este tutorial aprenderá:

¿Qué necesita?

Como dicta la tradición, el primer acercamiento que tendrá con el lenguaje de programación Kotlin es un "Hola mundo", el cual consiste en un programa muy sencillo que únicamente utiliza las librerías de entrada y salida del lenguaje de programación para saludar al usuario por medio de una interfaz de consola.

Para este tutorial, se utilizará una herramienta en línea que permite desarrollar código y ejecutarlo de forma similar a lo que se logra con el compilador por línea de comandos de Kotlin, la cual se denomina Kotlin playground. Ingrese a la herramienta en el siguiente enlace: https://play.kotlinlang.org/. Podrá notar que en la página web hay un editor para el código fuente, donde ya se encuentran las siguientes líneas de código:

fun main() {
    println("Hello, world!!!")
}

Estas líneas corresponden al programa "Hola mundo", donde se ejecuta la función main() con la instrucción println para imprimir un mensaje en consola. La palabra fun designa una función con el nombre main, la cual en este caso no toma ningún argumento (definidos entre paréntesis) por ejecutarse en un playground, pero que usualmente toma una lista de strings como parámetros. Esta función es especial y funciona igual que en el lenguaje Java, ya que es el principal punto de entrada de un programa básico de Kotlin y permite ejecutar cualquier serie de instrucciones definidas dentro de las llaves, las cuales determinan el funcionamiento de un programa.

No obstante, la función main del lenguaje Kotlin se distingue de otros lenguajes como Java, dado que no requiere especificar que el método no retorna ningún tipo de información. En Kotlin, estos métodos implícitamente retornan un objeto de la clase kotlin.Unit, el cual indica que solo se debe ejecutar una porción de código tal como se indica en el cuerpo del método.

En la página de playground utilizada para este tutorial encontrará un botón flotante de color azul, con el nombre Run en la parte superior derecha del editor. Para ejecutar el código, haga clic en este botón y podrá ver el saludo en la consola de comandos.

El primer elemento que estará presente en todas sus aplicaciones y programas es la información, por lo cual es importante reconocer la forma en que Kotlin le permite trabajar con tipos de variables y tipos de datos.

Tipos de datos en Kotlin

Al igual que en todos los lenguajes de programación tipados, Kotlin maneja tipos básicos numéricos, booleanos, arrays, caracteres individuales y cadenas de caracteres (strings). Los tipos de datos numéricos comprenden los Int, Byte, Short y Long o enteros, Double y Float o de punto flotante. Además de esto, Kotlin soporta literales numéricos en otros formatos como, por ejemplo, hexadecimal o binarios.

Las operaciones aritméticas básicas que soporta el lenguaje para los tipos numéricos comprenden los operadores +,-,/*,/,%. Una particularidad de Kotlin es que las variables numéricas, a pesar de ser tipos básicos, son almacenadas utilizando las clases Integer, Long, Double, etc. de la máquina virtual y, por ende, pueden ser receptoras de llamados a métodos como .toString(). En el caso de los tipos booleanos, también se cuenta con los operadores básicos de muchos lenguajes de programación ==, &&, ||, !. Y así mismo, estos también pueden ser receptores de llamados a métodos. Kotlin permite crear booleanos haciendo uso de las operaciones de comparación ==, <=, >=, <, >, is, entre otros. Los datos también pueden ser operados a nivel de bit con shifts y operaciones lógicas como xor, and, entre otras.

Tipos de variables en Kotlin

La declaración de las variables mutables se hace por medio de la siguiente sintaxis:

var <varId>: <type> = <value>
//Por ejemplo:
var i: Int = 13

Por su parte, las variables no mutables (values) se declaran con la siguiente sintaxis:

val <valId>: <type> = <value>
//Por ejemplo:
val s: String = "Hola Mundo"

Note que en ambos casos, las palabras reservadas var y val vienen seguidas de un identificador para el dato, la declaración del tipo de dato con el signo dos puntos, el operador de asignación y el valor correspondiente. Esta declaración toma dicha forma dado que Kotlin es un lenguaje fuertemente tipado, sin embargo esta declaración puede tener variaciones en las que no es necesario declarar el tipo de dato explícitamente, como, por ejemplo, cuando se está declarando un valor literal. En el caso de los valores numéricos, usualmente se especifica el tipo de dato en el número, como es el caso del número 1, 1.0, 123L, 0xFF, entre muchos otros. Otro caso en el que puede no ser necesario declarar el tipo de dato de forma explícita es cuando se asigna el resultado de una operación donde el tipo de dato es implícito, como, por ejemplo, una división entre números enteros, o las operaciones a nivel de bits.

Así mismo, otra variación de la declaración de variables puede darse con la palabra reservada lateinit, la cual sirve para instanciar una variable cuyo valor no se conoce en el momento de su declaración. En ese caso, es estrictamente necesario declarar un tipo de dato, de forma que la sintaxis se vería de la siguiente forma:

lateinit var <varId> : <type>
//Por ejemplo:
lateinit var found : String

La operación de asignación del valor de las variables de este tipo ocurriría posteriormente. No es posible, y no tiene sentido asignar el valor de una lateinit var durante su declaración.

Nota: la mutabilidad de las variables no incluye el tipo de dato. Es decir, no es posible cambiar el tipo de dato de una variable.

Finalmente, el último tipo de variables son las constantes, las cuales son values con un modificador adicional reconocido por la palabra reservada const. La diferencia entre const val y val es que el valor de const val se determina en tiempo de compilación y, por ende, no es posible asignar el resultado de una función.

Protección ante la nulidad

Un aspecto fundamental de Kotlin es la protección ante la nulidad o null safety, dado que una de las principales promesas de Kotlin es ofrecer un lenguaje donde se eviten los errores fundamentales de apuntadores nulos. Por este motivo, no existe el concepto de vacío que puede existir en otros lenguajes de programación y, en caso de ser necesario, por ejemplo, para el tipo de retorno de un método sin valor de retorno, se utiliza el tipo de dato Unit, que consiste en procedimientos o porciones de código ejecutables.

No obstante, en algunas ocasiones tiene sentido que las variables no tengan un valor puntual, por ejemplo, si dicha información se está cargando de alguna fuente. Para estos casos, Kotlin incluye varios operadores para soportar los valores nulos:

var marbles: Int? = null //Operador en el tipo de dato: soporte a valor null
fishFoodTreats = marbles?.dec() //Operador en el identificador: consulta valor null
if (marbles != null) {fishFoodTreats=marbles.dec()} //Código equivalente
fishFoodTreats = marbles?.dec() ?: 0

Estructuras complejas

Las listas también son tipos fundamentales de Kotlin y funcionan de forma similar a otros lenguajes de programación. La declaración de las listas se hace por medio del método listOf() para listas inmutables y por medio del método mutableListOf() para las listas mutables, en ambos casos indicando los elementos por parámetro (separados por coma). Los métodos más relevantes de las listas comprenden los siguientes:

Para conocer la lista completa de métodos disponibles en las listas, consulte el siguiente enlace para las listas inmutables: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/ y el siguiente enlace para las listas mutables: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-list/.

Similar a las listas, Kotlin cuenta con arrays, los cuales son contenedores de tamaño fijo con elementos sin un tipo especificado, declarados por medio del método arrayOf(). En caso de querer indicar el tipo de dato del array, existe un método para cada tipo de dato, como por ejemplo, intArrayOf(), con los elementos indicados por parámetro (separados por coma). Además de esto, la inicialización de los arrays puede hacerse programáticamente por medio de Units, como se muestra en el ejemplo a continuación:

val array = Array (5) { it * 2 }

En este caso, los métodos cambian con respecto a las listas, dado que no puede modificarse el tamaño de los arrays. Para consultar los métodos disponibles, consulte el siguiente enlace: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/.

Otra colección importante de Kotlin son los HashMaps, los cuales funcionan como diccionarios en otros lenguajes de programación o como listas de parejas clave-valor que permiten buscar de forma eficiente por llaves conocidas. Para declarar un HashMap, se utiliza el método hashMapOf() con las tuplas indicadas por parámetro, separadas por comas. Las tuplas se representan con la sintaxis <key> to <value>. Para acceder a los elementos del HashMap se puede utilizar el acceso con corchetes cuadrados [] o con el método .get(). Al igual que con las listas, existe una versión mutable de los HashMaps y una versión inmutable. Para consultar el detalle de ambas, consulte los siguientes enlaces: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-map/ y https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-hash-map/#kotlin.collections.HashMap.

Existen más colecciones y estructuras complejas en el lenguaje Kotlin, las cuales puede consultar en el siguiente enlace: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/.

Condicionales en Kotlin

Kotlin soporta las estructuras de control básicas que evalúan condiciones y ejecutan código según el resultado. A continuación se explica la sintaxis de las estructuras if-else y when (la cual reemplaza el tradicional switch-case de otros lenguajes de programación):

Bloques if-else

Los bloques if-else funcionan como en muchos lenguajes de programación. Deben partir de la palabra if, seguida de un valor booleano entre paréntesis, seguido de los corchetes con las instrucciones a ejecutar en cada caso, y se pueden encadenar con otras condiciones por medio de las palabras else if y, en el caso por defecto, la palabra else. A continuación se muestra la sintaxis general de estos if-else:

if (<condicion1>) {
    //cuerpo de instrucciones 1
} else if (<condicion2>) {
    //cuerpo de instrucciones 2
} else {
    //cuerpo de instrucciones - comportamiento predeterminado
}

Bloques when

Los bloques when son únicos del lenguaje Kotlin y son la alternativa a la tradicional estructura switch-case. La sintaxis es un poco distinta y es más legible para el programador. A continuación se muestra la sintaxis general de un bloque when:

when (<variable>) {
    <value1/range1> -> <cuerpo de instrucciones 1>
    <value2/range2> -> <cuerpo de instrucciones 2>
    else -> <cuerpo de instrucciones por defecto>
}

En ambos bloques se retorna un valor y, en caso de ser instrucciones procedimentales, se retorna un Unit. De esta forma, es posible utilizar los bloques if-else y los bloques when como una expresión y asignar su resultado a una variable de la siguiente forma:

var num = when (bool1) {
    true -> 100
    else -> 500
}

Iteraciones en Kotlin

Kotlin permite hacer ciclos iterativos con las clásicas estructuras for, while y do-while. Existe también el concepto de iterables, lo cual es una interfaz para darle a ciertas clases la cualidad de ser recorridos por porciones en un ciclo. Internamente Kotlin ofrece la posibilidad de recorrer utilizando estos iteradores con los operadores in. A continuación podrá conocer el detalle de la sintaxis de los ciclos mencionados anteriormente.

Ciclo for

Este ciclo tiene algunas variaciones, pero todas parten de indicar en los paréntesis siguientes a la palabra for una variable que va a contener el elemento actual de cada iteración, con el operador in y el elemento iterable a recorrer. Estos elementos iterables pueden ser rangos, colecciones, arrays, strings, entre otros. Así, el ciclo for en Kotlin es el equivalente al foreach de otros lenguajes de programación.

A continuación se muestra un par de ejemplos de ciclos for comúnmente utilizados:

for (item: Int in ints) {
    // ...
}

for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

Para conocer más sobre los rangos en Kotlin, ingrese al siguiente enlace: https://kotlinlang.org/docs/ranges.html.

Nota: el operador

in

funciona con el propósito de definir una variable para la iteración únicamente cuando se define dentro de un ciclo for. Este operador puede ser utilizado dentro de una estructura de control condicional para determinar si un elemento hace parte de un iterable.

Ciclos while y do-while

Estos ciclos funcionan de la misma forma que en otros lenguajes de programación, es decir, con base en una condición indicada por booleanos o iteradores y un cuerpo del ciclo donde necesariamente debe existir una posibilidad de que la condición se cumpla eventualmente. La diferencia entre el ciclo while y el ciclo do-while consiste en que la condición se evalúa desde antes de la primera iteración para el ciclo while, y en el ciclo do-while se evalúa después de la primera iteración sobre el cuerpo del ciclo. A continuación se muestra un ejemplo de la sintaxis en cada uno de los casos:

while (<condición>) {
    <instrucción de avance del ciclo>
}

do {
    val y = retrieveData()
} while (y != null)

Además de su fuerte soporte al paradigma de programación funcional, Kotlin ofrece soporte al paradigma de POO, y en un gran número de librerías y aplicaciones es fundamental comprender su funcionamiento para poder desarrollar programas. Los conceptos básicos son los mismos que existen para este paradigma en cualquier lenguaje de programación; los términos clase, objeto, propiedad, método, interfaz y paquete conservan el mismo significado en el caso de Kotlin. Las secciones a continuación indican cómo se implementan los conceptos fundamentales en el lenguaje de programación Kotlin.

Clases

Las clases en su nivel más básico son declaradas con la palabra reservada class y el nombre de la clase. En una aplicación estructurada, una clase debe estar definida en un archivo donde se indique el package al que pertenece, de forma que el código fuente del proyecto esté encapsulado por medio de divisiones semánticas. A continuación se verán los aspectos más importantes de una clase en Kotlin:

Propiedades y métodos

La información que caracteriza a una clase se denomina propiedades, lo cual comprende cualquier serie de variables y constantes requeridas para modelar la clase. Además de esto, una clase se caracteriza por sus métodos, los cuales son funciones que permiten realizar operaciones relacionadas a la clase. Ambos elementos conforman el cuerpo de la clase y van a permitir que los objetos que se creen a partir de dicha clase conserven el mismo comportamiento para sus diferentes posibles estados.

En cuanto a las propiedades, existen múltiples niveles de privacidad o acceso a los datos, los cuales funcionan de forma similar a los modificadores de visibilidad de Java. En el caso de Kotlin, existen los siguientes modificadores para propiedades y para métodos de una clase:

Además de propiedades y métodos, las clases también pueden contener objetos estáticos, lo cual facilita la implementación de patrones de diseño como Singleton. En este caso, Kotlin ofrece los companion objects, los cuales son declaraciones de objetos dentro de una clase que contienen información que va a ser compartida por todas las instancias de la clase, y su sintaxis es la siguiente:

companion object Factory {
        //anything
    }

Constructores e inicialización

Para poder instanciar objetos de una clase es necesario definir métodos constructores. En Kotlin existen múltiples formas de hacer un método constructor, siendo la más básica simplemente definir las propiedades de la clase, separadas por comas, entre unos paréntesis luego de la declaración del nombre de la clase, agregando la palabra constructor, como se muestra a continuación:

class Person constructor(firstName: String, lastName:String) { /*...*/ }

En estos constructores básicos no puede existir código de inicialización. Para lograr la inicialización de los objetos de una clase, es necesario utilizar, dentro del cuerpo de la clase, los bloques de inicialización, los cuales se crean utilizando la palabra reservada init. La inicialización suele requerir los parámetros del método constructor. Estos pueden ser utilizados de forma libre dentro de la clase.

Adicionalmente, existen métodos constructores secundarios, los cuales pueden ampliar los métodos ya existentes con diferentes implementaciones. Para crear un método constructor secundario, dentro del cuerpo de la clase se define con la siguiente sintaxis:

constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }

Herencia

Otro concepto fundamental de la programación orientada a objetos es la herencia. En Kotlin, todas las clases heredan de la clase Any por defecto, la cual le brinda a todos los objetos los tres métodos en común: equals(), hashCode() y toString(). Únicamente las clases que sean declaradas con la palabra reservada open pueden ser heredadas por otras. Para declarar un supertipo de una clase, en la declaración inicial de la clase, es necesario añadir, antes de su cuerpo, la superclase de la cual va a heredar de la siguiente forma:

open class Base(p: Int) //Definida en algún lado
//...
class Derived(p: Int) : Base(p)


Note que en los constructores de la clase derivada, es necesario inicializar el tipo base con su método constructor correspondiente. Esto aplica para constructores primarios (como el del código anterior), como para constructores secundarios, donde es necesario llamar de forma explícita al método super(args).

Para las clases que heredan, muchas veces será necesario sobreescribir los métodos que se definieron en la superclase para brindar una implementación más específica. En este caso, la clase base debe abrir los métodos y propiedades que pueden ser sobreescritos con la palabra reservada open. Luego, en la clase derivada, será necesario declarar el método o propiedad con el mismo nombre de su superclase, con la palabra override, como se muestra a continuación:

open class Shape {
    open val vertexCount: Int = 0
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override val vertexCount = 4
    override fun draw() { /*...*/ }
}

Además de la sobreescritura, los métodos implementados por la superclase pueden ser llamados explícitamente por medio del objeto super.

Tipos de clases

Además de los aspectos mencionados anteriormente, las clases pueden tener diversas variaciones, como lo son:

Además de estas, existen diferentes modificadores para las clases, los cuales puede consultar en el siguiente enlace: https://developer.android.com/codelabs/kotlin-bootcamp-classes.

En el caso de este tutorial, se propone una aplicación sencilla de interacción por consola donde usted pueda reforzar los conceptos que aprendió a lo largo de los anteriores pasos. La aplicación consistirá en un programa sencillo que almacena las notas de un listado de estudiantes de un curso donde las definitivas se calculan según un promedio de notas con peso asignado.

Para lograr esta aplicación es necesario crear varias clases que representen las entidades básicas, las cuales son Nota, Estudiante y Clase. Se desea que la nota tenga un valor numérico y un peso porcentual del promedio de la clase. En cuanto al estudiante, es necesario tener un nombre completo, un identificador y un arreglo de notas. Por último, en cuanto a la clase, es necesario tener un listado de estudiantes y un nombre para la materia. Es necesario asignar los estudiantes a las clases y las notas a los estudiantes después de las inicializaciones de cada clase.

Al final de este tutorial, debe llegar a un resultado como el siguiente en el playground:

data class Nota(val num:Double, val peso:Double)

class Estudiante constructor(firstName: String, lastName:String, id:Int) { 
        private val firstName :String;
          private val lastName :String;
    val id :Int;
    private val notas :MutableList<Nota>;

    init{
        this.firstName = firstName;
        this.lastName = lastName;
        this.id = id;
        this.notas = mutableListOf();
    }
    override fun toString() : String{
        return "(${id}) ${firstName} ${lastName}"
    }
    fun getAvg():Double{
        var sum = 0.0;
        for(nota in notas){
            sum+=nota.num*nota.peso
        }
        return sum
    }
    fun agregarNota(n: Nota){
        this.notas.add(n)
    }
    
}

class Clase constructor(curso: String){
    private val curso :String;
    private val estudiantes:MutableList<Estudiante> = mutableListOf();
    init{
        this.curso = curso;
    }
    fun calificar(notas:HashMap<Int,Nota>){
        if(notas.size == estudiantes.size){
            for(estudiante in estudiantes){
                estudiante.agregarNota(notas[estudiante.id]!!)
            }
        }
    }
    fun matricularEstudiantes(nuevos:Collection<Estudiante>){
        estudiantes.addAll(nuevos)
    }
    fun verPromediosNotas():String{
        var resp = "";
        if(estudiantes.size>0){
            for(estudiante in estudiantes){
                    resp += "${estudiante.toString()}: ${estudiante.getAvg()}\n"
            }
        }else{
            resp = "No hay estudiantes"
        }
        return resp
    }
}

fun main(args : Array<String>) {
    var curso = Clase("Moviles")
    var listaEstudiantes = listOf(Estudiante("Luis","Aragones",1),Estudiante("Fernando","Hierro",2),Estudiante("Raul","Gonzalez",3),Estudiante("Fernando","Morientes",4))
    curso.matricularEstudiantes(listaEstudiantes)
    curso.calificar(hashMapOf(1 to Nota(3.5,0.3), 2 to Nota(4.0,0.3), 3 to Nota(4.5,0.3), 4 to Nota(4.6,0.3)))
    curso.calificar(hashMapOf(1 to Nota(4.0,0.3), 2 to Nota(5.0,0.3), 3 to Nota(4.2,0.3), 4 to Nota(4.8,0.3)))
    curso.calificar(hashMapOf(1 to Nota(4.0,0.3), 2 to Nota(5.0,0.3), 3 to Nota(4.3,0.3), 4 to Nota(5.0,0.3)))
    curso.calificar(hashMapOf(1 to Nota(5.0,0.1), 2 to Nota(4.4,0.1), 3 to Nota(4.0,0.1), 4 to Nota(5.0,0.1)))
        println(curso.verPromediosNotas())
}

Explore el código anterior en detalle y note que al inicio existe una línea donde se declara una clase de datos para las notas. En este caso, la clase solamente contiene un constructor por defecto a partir de los atributos indicados.

Posteriormente, se define la clase de un estudiante, la cual utiliza un constructor que requiere el nombre, el apellido y el identificador para inicializarlas en el bloque init. Esta clase contiene una propiedad para la lista de notas del tipo MutableList, de forma que pueda crecer de tamaño de forma indefinida (como esta no es inherente al estudiante, su valor en la inicialización únicamente consiste en una lista vacía). Además, se puede ver redefinido el método toString(), con la anotación override para utilizar este método en lugar del método definido en la superclase Any. Así mismo, se define un método para obtener el promedio de notas, el cual se calcula con un ciclo for que recorre los elementos del iterable notas, y un método para agregar una nota a la vez.

Luego de esto, se define la clase de una clase o curso de varios estudiantes. El método constructor de esta funciona de la misma forma que el de la clase Estudiante, dado que contiene una propiedad que es un listado de estudiantes, la cual no es inherente a la clase misma. Además, esta clase cuenta con un método calificar para agregar varias notas a los estudiantes del curso a partir de un HashMap, el cual se utiliza para poder asignar las notas por medio de tuplas que relacionan el id de un estudiante con su nota. También hay un método matricularEstudiantes, el cual permite concatenar un conjunto de estudiantes al listado de la clase, y un método para ver el promedio de notas de todos los estudiantes de un curso.

Finalmente, se define un método main donde se utilizan datos de prueba para probar la corrección del programa.

Ejecute el programa. Debería ver en consola el siguiente resultado:

(1) Luis Aragones: 3.95
(2) Fernando Hierro: 4.640000000000001
(3) Raul Gonzalez: 4.3
(4) Fernando Morientes: 4.82

¡Felicidades!

Al finalizar este tutorial, pudo familiarizarse con la programación en Kotlin y pudo desarrollar una aplicación sencilla donde aplicó los conceptos aprendidos de forma sencilla e interactiva, gracias al playground de Kotlin online.

Ahora podrá utilizar el lenguaje de programación Kotlin en múltiples entornos, como, por ejemplo, el desarrollo de aplicaciones Android.

Créditos

Versión 1.0 - Mayo 30, 2021

Juan Sebastián Espitia Acero

Autor

Norma Rocio Héndez Puerto

Revisora

Mario Linares Vásquez

Revisor