Kotlin es un lenguaje de programación de código abierto de tipo estático que se dirige a JVM, Android, JavaScript y Native. Está desarrollado por JetBrains. El proyecto se inició en 2010 y fue de código abierto desde muy temprano. El primer lanzamiento oficial 1.0 fue en febrero de 2016.
Un lenguaje de programación se diseña generalmente con un propósito específico en mente. Este propósito puede ser cualquier cosa, desde servir a un entorno específico (por ejemplo, la web) hasta un determinado paradigma (por ejemplo, la programación funcional). En el caso de Kotlin el objetivo es construir un lenguaje productivo y pragmático, que tenga todas las características que un desarrollador necesita y que sea fácil de usar.
Kotlin fue diseñado inicialmente para trabajar con otros lenguajes de JVM, pero ahora ha evolucionado para ser mucho más: también funciona en el navegador y como una aplicación nativa.
Kotlin es multiparadigma, con soporte para paradigmas de programación orientada a objetos, procedimentales y funcionales, sin forzar el uso de ninguno de ellos. Por ejemplo, a diferencia de Java, puede definir funciones de nivel superior, sin tener que declararlas dentro de una clase.
;
. Los bloques de código están delimitados por corchetes
{ }
.static
, sino que hay mejores
alternativas.if
, for
, etc… Todos
pueden devolver valores.when
es como un interruptor con
superpoderes.Más información:
$ kotlinc name.kt -include-runtime -d name.jar
$ java -jar name.jar
$ kotlinc-jvm
$ kotlinc -script name.kts [params]
$ kotlinc name.kt -d name.jar
$ kotlin -classpath name.jar HelloKt (HelloKt is the main class name inside the file named name.kt)
Más información:
El punto de entrada en un programa escrito en Kotlin (y en Java) es
la función main(args: Array<String>)
. Esta función
recibe un array que contiene los argumentos de la línea de comandos.
fun main(args: Array<String>) {
("Hello World!")
println}
Las funciones y variables en Kotlin pueden declararse en un “nivel superior”, es decir, directamente dentro de un paquete.
Si un archivo Kotlin contiene una sola clase (potencialmente con
declaraciones de nivel superior relacionadas), su nombre debe ser el
mismo que el nombre de la clase, con la extensión ‘.kt’. Si un archivo
contiene varias clases, o solo declaraciones de nivel superior, el
nombre debe describir lo que contiene el archivo en formato
‘UpperCamelCase’ (e.g. ProcessDeclarations.kt
)
Kotlin sigue las convenciones de nomenclatura de Java. Los nombres de
los paquetes se escriben siempre en minúsculas y sin guiones bajos
(e.g. org.example.myproject
)
Los nombres de las clases y los objetos se escriben en ‘UpperCamelCase’:
open class DeclarationProcessor { ... }
object EmptyDeclarationProcessor : DeclarationProcessor() { ... }
Los nombres de funciones, propiedades y variables locales en ‘lowerCamelCase’:
fun processDeclarations() { ... }
var declarationCount = ...
Los nombres de las constantes (propiedades marcadas con
const
) deben usar nombres en mayúsculas y separados por un
guión bajo:
const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"
En Kotlin, todo es un objeto en el sentido de que podemos llamar funciones y propiedades de miembro en cualquier variable. Algunos de los tipos como los números, los caracteres o los booleanos pueden tener una representación interna especial que se representa como valores primitivos en tiempo de ejecución, pero para el usuario se comportan como clases ordinarias.
La declaración de valores se realiza utilizando var
o
val
:
Los valores constantes se declaran como
val
y son inmutables o ‘read-only’, es decir, que
no se pueden reasignar.
Las variables se declaran como var
y son mutables, por lo que se le pueden asignar un nuevo valor pero
únicamente del mismo tipo declarado.
La recomendación es crear valores constantes inmutables, que son más seguras en entornos ‘multithreading’ ya que no se pueden modificar y utilizar las variables mutables cuando sea necesario.
Este soporte de primera clase para los valores constantes es importante por una razón: la programación funcional. En la programación funcional, el uso de los valores constantes permiten algunas optimizaciones que aumentan el rendimiento. Por ejemplo, los cálculos pueden ser paralelos ya que existe una garantía de que el valor no cambiará entre dos ejecuciones paralelas, dado que no puede cambiar.
val fooVal = 10 // val es inmutable y no podrá ser reutilizada
val otherVal
= "My Value" // Podemos declarar la variable 'val' en una línea y asignarle valor posteriormente. Sigue siendo una sola asignación.
otherVal
var fooVar = 10
= 20 // Se le puede asignar un nuevo valor pero únicamente del mismo tipo. fooVar
En la mayoría de los casos, Kotlin puede determinar o inferir cuál es el tipo de una variable, por lo que no tenemos que especificarla explícitamente. Cuando la variable no se inicialice deberemos indicar explícitamente el tipo de la variable ya que Kotlin no puede inferir el tipo si no se inicializa.
val foo: Int = 7
val bar = 10 // Kotlin infiere automáticamente el tipo
val hello: String // Si no se inicializa hay que especificar el tipo
Kotlin proporciona los tipos Byte
, Short
,
Int
y Long
para enteros y los tipos
Float
y Double
para números en coma
flotante:
val double: Double = 64.0 // 64 bits
val float: Float = 32.0F // or 32f (32 bits)
val long: Long = 64L // 64 bits
val int: Int = 32 // 32 bits
val short: Short = 16 // 16 bits
val byte: Byte = 8 // 8 bits
val hexadecimal: Int = 0x16
val binary: Int = 0b101
val char: Char = 'a'
Todas las variables inicializadas con un entero no deben exceder el
tamaño máximo de Int
ya que Kotlin infiere el tipo
Int
si no se especifica explícitamente el tipo o se añade
el apéndice ‘L’ al valor. En el caso de números en coma flotante, Kotlin
infiere el tipo Double
si no se indica el tipo
explícitamente o se marca el valor en coma flotante con el apéndice
‘F’.
val a = 1 // Kotlin infiere el tipo 'Int'
val b = 1L // Kotlin infiere el tipo 'Long'
val c = 3.14 // Kotlin infiere el tipo 'Double'
val d = 2.7123F // Kotlin infiere el tipo 'Float'
A diferencia de Java, en Kotlin todos los tipos son
objetos y por tanto no hay ‘wrappers’ u objetos
envoltorio tipo Integer
, Double
, etc…
Los guiones bajos se pueden utilizar para hacer que los números grandes sean más legibles:
val million = 1_000_000
La conversión debe ser invocada explícitamente. Hay conversiones desde un tipo al resto de tipos:
toByte()
➜ BytetoShort()
➜ ShorttoInt()
➜ InttoLong()
➜ LongtoFloat()
➜ FloattoDouble()
➜ DoubletoChar()
➜ Charval otherLong = int.toLong()
val direct = 25.toLong()
Los caracteres no son números en Kotlin, a diferencia de Java. En
Kotlin los caracteres se representan con el tipo Char
:
Los literales de carácter se escriben con comillas simples como por
ejemplo 'a'
. Los caracteres especiales se escapan con la
barra invertida '\'
. Están soportadas las siguientes
secuencias de escape: \t
, \b
, \n
,
\r
, \'
, \"
, \\
,
\$
.
Podemos convertir de forma explícitia un carácter en un número de
tipo Int
:
fun decimalDigitValue(c: Char): Int {
if (c !in '0'..'9')
throw IllegalArgumentException("Out of range")
return c.toInt() - '0'.toInt() // Explicit conversions to numbers
}
Las cadenas son secuencias de caracteres inmutables
y se representan con el tipo String
de manera similar a
Java. Las cadenas se crean usando las comillas dobles. El escapado de
caracteres se hace con una barra invertida '\'
.
val fooString = "My String Is Here!"
val barString = "Printing on a new line?\nNo Problem!"
val bazString = "Do you want to add a tab?\tNo Problem!"
(fooString)
println(barString)
println(bazString)
println("John Doe"[2]) // => h
println("John Doe".startsWith("J")) // => true println
Se puede acceder a los elementos de una cadena como si fuera un array
(e.g. s[i]
) e iterar con un bucle tipo
for
:
for (c in str) {
(c)
println}
Se puede utilizar el operador +
para concatenar cadenas
entre sí y con valores de otro tipo siempre y cuando uno de los
elementos de la expresión sea una cadena:
val s = "abc" + 1
(s + "def") println
Una cadena sin formato o ‘raw string’ está delimitada por una comilla triple (“““). Las cadenas sin formato pueden contener nuevas líneas y cualquier otro carácter. Estas cadenas sin formato también tiene soporte para las ‘string templates’:
val fooRawString = """
fun helloWorld(val name : String) {
println("Hello, world!")
}
val hello = $who
val result = ${2 + 2}
"""
Con la función trimMargin()
podemos eliminar los
espacios en blanco:
val text = """
|Tell me and I forget.
|Teach me and I remember.
|Involve me and I learn.
|(Benjamin Franklin)
""".trimMargin()
Un literal de cadena puede contener expresiones de plantilla o ‘template expressions’, que son fragmentos de código que será evaluado y cuyo resultado será concatenado en la cadena. Son una forma simple y efectiva de incrustar valores, variables o incluso expresiones dentro de una cadena.
Una expresión de plantilla comienza con un signo de dólar
($
) y consisten en un nombre de una variable (por ejemplo
$i
) o en una expresión (como por ejemplo
${name.length}
) en cuyo caso se utilizan llaves
({}
):
val name = "John Doe"
("$name has ${name.length} characters") // => John Doe has 8 characters
println
val age = 40
("You are ${if (age > 60) "old" else "young"}") // => You are young println
Las plantillas son compatibles tanto dentro de cadenas sin procesar como dentro de cadenas escapadas. En caso de necesitar representar el literal del dólar en una cadena sin escapar se utiliza esta sintaxis:
val price = """
${'$'}9.99
"""
Una matriz está representada por la clase Array
y es
invariante, por lo que, por ejemplo, no se puede
asignar un Array<String>
a un tipo de variable
Array<Any>
.
En Kotlin, podemos crear una matriz de elementos del mismo tipo o de
distinto tipo utilizando la función de biblioteca arrayOf()
y pasándole los elementos a añadir:
val cardNames = arrayOf("Jack", "Queen", "King", 3, false)
(cardNames[1]) // => Queen println
Podemos forzar la creación de arrays del mismo tipo. De esta forma el compilador comprobará el tipo de los elementos que se añaden y evitará que se añadan elementos de tipos no válidos:
val myArray = arrayOf<Int>(1, 2, 3, 4)
(myArray.contentToString()) // => [1, 2, 3, 4] println
La biblioteca estándar de Kotlin provee funciones para crear arrays
de tipos primitivos como intArrayOf()
,
longArrayOf()
, charArrayOf()
,
doubleArrayOf()
, etc… Cada una de estas funciones devuelven
una instancia de su equivalente en Kotlin como IntArray
,
LongArray
, CharArray
,
DoubleArray
, etc…:
val cards = intArrayOf(10, 11, 12) // IntArray
("${cards[1]}") // => 11 println
Para mejorar la eficiencia y rendimiento del código, cuando se
utilicen tipos primitivos hay que utilizar las funciones
intArrayOf()
, longArrayOf()
, etc.. en vez de
arrayOf()
para así evitar el coste asociado a las
operaciones de ‘boxing’/‘unboxing’.
Alternativamente, podemos crear una matriz a partir de un tamaño
inicial y una función, que se utiliza para generar cada elemento usando
el constructor Array()
:
val allCards = Array(12, { i -> i + 1 })
("${allCards.first()} - ${allCards.last()}") // => 1 - 12 println
Iterando sobre la matriz con indices
:
for (index in cardNames.indices) {
("Element $index is ${cardNames[index]}")
println}
Otra forma posible de iterar es usando la función
withIndex()
:
for ((index, value) in cardNames.withIndex()) {
("$index - $value")
println}
La palabra clave package
funciona de la misma manera que
en Java. El nombre del paquete se usa para construir el “Fully
Qualified Name” (FQN) de una clase, objeto, interfaz o
función.
Todo el contenido (como clases y funciones) de un fichero fuente están contenidos en el paquete declarado. Los nombres de los paquetes se escriben en minúscula y sin guiones bajos:
package com.example.kotlin
class MyClass { /*...*/ }
fun saySomething(): String { /*...*/ }
En el ejemplo, el FQN de la clase será
com.example.kotlin.MyClass
.
Dado que podemos tener ‘top-level functions’ como la función
saySomething()
del ejemplo, el FQN de esta función será
com.example.kotlin.saySomething
.
Si no se especifica un paquete, el contenido del fichero fuente pertenece al paquete ‘default’.
En Kotlin, usamos la declaración de importación para permitir que el compilador localice las clases e interfaces, propiedades, enumeraciones, funciones y objetos que se importarán.
En Java, por otro lado, solo esta permitido importar clases o interfaces.
// 'Bar' esta disponible en el código
import foo.Bar
// Si existe cierta ambigüedad podemos usar la palabra clave 'as'
import foo.Bar
import bar.Bar as bBar
// Todo el contenido de 'foo' está disponible
import foo.*
Por defecto, al igual que en Java, el compilador importa de forma implícita una serie de paquetes y por tanto están disponibles de forma automática.
// Single-line comments start with //
/*
Multi-line comments look like this.
*/
Kotlin tiene 4 construcciones de control de flujo: if
,
when
, for
y while
.
if
y when
son expresiones, por lo que
devuelven un valor; for
y when
son
declaraciones, por lo que no devuelven un valor. if
y
when
también se pueden utilizar como sentencias, es decir,
se pueden utilizar de forma autónoma y sin devolver un valor.
Un bucle for
puede usarse con cualquier elemento que
proporcione un iterador como rangos, colecciones, etc…:
for (c in "hello") {
(c)
println}
for (i in 1..3) {
(i)
println}
for (i in 6 downTo 0 step 2) {
(i)
println}
Los bucles while
y do-while
funcionan de la
misma manera que en otros lenguajes:
while (x > 0) {
--
x}
do {
val y = retrieveData()
} while (y != null) // y is visible here!
La instrucción if
y if..else
funciona igual
que en Java. Además, en Kotlin los bloques if
se pueden
utilizar como una expresión que devuelve un valor. Por este motivo el
operador ternario ‘condition ? then: else’ no es necesario en
Kotlin:
// Traditional usage
var max = a
if (a < b) max = b
// With else
var max: Int
if (a > b) {
= a
max } else {
= b
max }
// As expression
val max = if (a > b) a else b
// With blocks
// returns a or 5
var top = if (a > 5) {
("a is greater than 5")
println
a} else {
("5 is greater than a")
println5
}
Los bloques when
se pueden usar como una alternativa a
las cadenas if-else-if
o en substitución de los
switch
. Si no se proporciona ningún argumento, las
condiciones de la rama son simplemente expresiones booleanas, y una rama
se ejecuta cuando su condición es verdadera:
when {
.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
xelse -> print("x is funny")
}
La instrucción when
se puede usar con un argumento. Si
ninguna de las opciones coincide con el argumento, se ejecuta la opción
del bloque else
:
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> {
("none of the above") // Nótese el uso de llaves para delimitar el bloque de código
println}
}
La instrucción when
se puede utilizar como una expresión
que devuelve un valor. En este caso el bloque else
es
obligatorio. De hecho, la única excepción a esta regla
es si el compilador puede garantizar que siempre devuelve un valor. Por
lo tanto, si las ramas normales cubren todos los valores posibles,
entonces no hay necesidad de una rama else
:
val result = when (i) {
0, 21 -> "0 or 21"
in 1..20 -> "in the range 1 to 20"
else -> "none of the above"
}
(result)
println
val check = true
val result = when(check) { // All results are covered
true -> println("it's true")
false -> println("it's false")
}
Se pueden utilizar expresiones arbitrarias, y no solo constantes, como condiciones en los bloques:
when (x) {
(s) -> print("s encodes x")
parseIntelse -> print("s does not encode x")
}
Si muchos casos deben manejarse de la misma manera, las condiciones de la rama pueden combinarse con una coma:
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
También podemos verificar si un valor está dentro in
o
no está dentro !in
de un rango o una colección:
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
Las funciones se declaran usando la palabra clave 'fun'
.
Los nombres de las funciones empiezan con minúscula. Los parámetros de
la función se especifican entre paréntesis después del nombre de la
función y tienen la forma 'name: type'
. El tipo de
cada parámetro debe especificarse explícitamente y no puede
omitirse.
fun powerOf(number: Int, exponent: Int) { ... }
Los parámetros de la función pueden tener opcionalmente un valor por defecto, que se utilizará en caso de se omita el argumento al invocar la función. El tipo de retorno de la función, si es necesario, se especifica después de los parámetros:
fun hello(name: String = "world"): String { // valor por defecto
return "Hello, $name!"
}
("foo") // => Hello, foo!
hello(name = "bar") // => Hello, bar!
hello() // => Hello, world!
hello
fun bye(bye: String = "Bye", name: String): String {
return "$bye, $name!!"
}
(name = "John", bye = "Good bye") // => Good bye, John!!
bye(name = "John") // => Bye, John!! bye
En la sobreescritura de métodos con valores por defecto siempre se utilizan los mismos valores de parámetros por defecto que el método base. Cuando se sobreescribe un método, los valores por defecto deben omitirse de la firma:
open class A {
open fun foo(i: Int = 10) { ... }
}
class B : A() {
override fun foo(i: Int) { ... } // no default value allowed
}
Si un parámetro por defecto precede a un parámetro sin valor predeterminado, el valor por defecto solo se puede usar llamando a la función con argumentos con nombre:
fun foo(bar: Int = 0, baz: Int) { ... }
(baz = 1) // The default value bar = 0 is used foo
Dado que Java no admite valores de parámetros por defecto en los
métodos, deberá especificar todos los valores de parámetros
explícitamente cuando llame a una función de Kotlin desde Java. Kotlin
nos proporciona la funcionalidad para facilitar las llamadas de Java al
anotar la función Kotlin con '@JvmOverloads'
. Esta
anotación le indicará al compilador de Kotlin que genere las funciones
sobrecargadas de Java para nosotros.
@JvmOverloads
fun calCircumference(radius: Double, pi: Double = Math.PI): Double = (2 * pi) * radius
// En Java
(double radius, double pi);
double calCircumference(double radius); double calCircumference
Cuando una función no devuelve ningún valor significativo, su tipo de
devolución por defecto es Unit
. En ese caso indicar el tipo
de retorno es opcional. El tipo Unit
es un objeto en Kotlin
que es similar a los tipos void
en Java y C.
fun hello(name: String): Unit {
("Hello $name")
print}
fun sayHello(name: String) { // compila ya que el compilador infiere el tipo 'Unit'
("Hello $name")
print}
Los parámetros con nombre permiten código más legible al nombrar los parámetros que se pasan a una función cuando se invoca. Una vez que se utiliza un nombre en un parámetro, el resto de parámetros también deben asignarse con nombre:
fun area(width: Int, height: Int): Int {
return width * height
}
(10, 12)
area(width = 10, height = 12) // código más legible
area(height = 12, width = 10) // podemos cambiar el orden
area(10, height = 12) // argumento por posición y argumentos con nombre
area(width = 10, 12) // ¡incorrecto! no se permiten argumentos con nombre antes de argumentos por posición
area
fun bar(k: Int, m: Long = 1L, j: Boolean = true) = println("$k - $m - $j")
// Una vez que un parámetro ha sido nombrado, todos los siguientes parámetros deben ser nombrados
(10) // => Se omiten los parámentros por defecto
bar(15, 30L)
bar(20, 2L, true)
bar(m = 30L, j = false, k = 10)
bar(k = 10, m = 20L, j = true)
bar(5, m = 2L, j = true)
bar(6, 1L, j = true) bar
Cuando se invoca una función con argumentos posicionales y con
nombre, todos los argumentos posicionales deben colocarse antes del
primero argumento con nombre. Por ejemplo, la llamada
f(1, y = 2)
está permitida, pero f(x = 1, 2)
no está permitida.
Para pasar un número variable de argumentos a una función podemos
usar la palabra clave 'vararg'
delante del nombre de una
variable. Por tanto la función aceptará una lista de parámetros
separados por comas que el compilador envolverá en una array. Por tanto,
dentro de la función accederemos a los parámetros mediante la notación
de array.
Este tipo de parámetros se puede combinar con otros parámetros.
Normalmente el parámetro 'vararg'
será el último de la
lista. Si hay otros parámetros después de 'vararg'
, deberán
usarse parámetros con nombre:
fun varargExample(vararg names: Int) {
("Argument has ${names.size} elements")
println}
() // => Argument has 0 elements
varargExample(1) // => Argument has 1 elements
varargExample(1, 2, 3) // => Argument has 3 elements
varargExample
fun car(vararg model: String, year: Int) {}
("Audi", "A6", year = 2005) // parámetros con nombre después de 'vararg' car
Para utilizar un array para suministrar un número variable de
argumentos se utiliza el operador '*'
también llamado
‘spread operator’ delante del nombre de la variable del
array:
val intArray = intArrayOf(1, 2, 3, 4)
val array = Array(5, { i -> i + 1 })
(*intArray) // => Argument has 4 elements
varargExample(*array.toIntArray()) // => Argument has 5 elements varargExample
Cuando una función consiste en una sola expresión, se pueden omitir
los paréntesis. El cuerpo se especifica después de un símbolo
'='
:
fun odd(x: Int): Boolean = x % 2 == 1
Declarar explícitamente el tipo de retorno de una función cuando es
una expresión es opcional cuando puede ser inferido por el compilador o
cuando el tipo de retorno es 'Unit'
. Cuando el cuerpo de
una función es un bloque hay que especificar el tipo de retorno ya que
el compilador no puede inferirlo:
fun even(x: Int) = x % 2 == 0 // Optional
fun printHello(name: String?) { // 'Unit'
if (name != null)
("Hello ${name}")
printlnelse
("Hi there!")
println// `return Unit` or `return` is optional
}
A veces queremos devolver múltiples valores desde una función. Una
forma es usar el tipo 'Pair'
de Kotlin. Esta estructura
incluye dos valores a los que luego se puede acceder. Este tipo de
Kotlin puede aceptar cualquier tipo que suministre a su constructor. Y,
lo que es más, los dos tipos ni siquiera necesitan ser iguales. Kotlin
también provee el tipo 'Triple'
que retorna tres
valores:
fun getNumbers(num: Int): Pair<Int?, Int?> {
(num > 0, { "Error: num is less than 0" })
requirereturn Pair(num, num * 2)
}
val(num, num2) = getNumbers(10) // destructuring
En Kotlin, podemos hacer que la creación de una instancia ‘Pair’ sea más compacta y legible utilizando la función ‘to’, que es una función ‘infix’ en lugar del constructor de ‘Pair’.
val nigeriaCallingCodePair = 234 to "Nigeria"
val nigeriaCallingCodePair2 = Pair(234, "Nigeria") // Same as above
Las ‘extension functions’ son una forma de agregar nuevas funcionalidades a una clase sin tener que heredar de dicha clase. Esto es similar a los métodos de extensión de C#. Una función de extensión se declara fuera de la clase que quiere extender. En otras palabras, también es una ‘top-level function’. Junto con las funciones de extensión, Kotlin también admite propiedades de extensión.
Para crear una ‘extension function’, debe prefijar el nombre de la clase que está extendiendo antes del nombre de la función. El nombre de la clase o el tipo en el que se define la extensión se denomina tipo de receptor, y el objeto receptor es la instancia de clase o el valor concreto sobre el que se llama a la función de extensión.
fun String.remove(c: Char): String { // 'String' es el tipo receptor
return this.filter { it != c } // 'this' corresponde al objeto receptor
}
("Hello, world!".remove('l')) // => Heo, world! // "Hello World" es el objeto receptor println
En caso de que una ‘extension function’ tenga la misma firma (mismo nombre y misma lista de parámetros) que una función miembro, es decir, una función de la clase, el compilador invocará antes la función miembro que la función de extensión aunque no se generará ningún error de compilación:
class C {
fun foo() { println("member") }
}
fun C.foo() {
("extension")
println}
fun C.foo(i: Int) {
("extension & overrided")
println}
().foo() // => member
C().foo(5) // => extension & overrided C
Las funciones de nivel superior son funciones que se definen fuera de cualquier clase, objeto o interfaz. Esto significa que son funciones a las que llama directamente, sin la necesidad de crear ningún objeto o llamar a ninguna clase. Dado que Java no soporta este tipo de funciones el compilador de Kotlin genera una clase con métodos estáticos. Estas tipo de funciones son especialmente útiles para crear funciones de utilidad o de ayuda.
// Code defined inside a file called 'UserUtils.kt'
@file:JvmName("UserUtils")
package com.example.project.utils
fun checkUserStatus(): String {
return "online"
}
Las funciones en Kotlin son de primera clase, lo que significa que pueden ser almacenadas en variables y estructuras de datos, pasadas como argumentos y devueltas desde otras funciones de orden superior. Puede operar con funciones de cualquier manera que sea posible para otros valores no funcionales.
Para facilitar esto, Kotlin, como lenguaje de programación estáticamente tipado, utiliza una familia de tipos de función para representar funciones y proporciona un conjunto de construcciones de lenguaje especializadas, tales como expresiones lambda.
Una ‘high-order function’ o función de orden superior es una función que puede tomar funciones como parámetros y/o devolver una función como tipo de retorno.
// Función con dos parámetros, el segundo de ellos es una función
fun foo(str: String, fn: (String) -> String): Unit {
val applied = fn(str)
(applied)
println}
("Hello", { it.reversed() }) // => olleH
foo
// Esta función de orden superior devuelve una función
fun isPositive(n: Int): (Int) -> Boolean {
return { n > 0 } // return a function. Instead 'return value' we have 'return { function }'
}
// Esta función de orden superior devuelve una función de forma más compacta
fun modulo(k: Int): (Int) -> Boolean = { it % k == 0 }
val evens = listOf(1, 2, 3, 4, 5, 6).filter(modulo(2)) // => [2, 4, 6]
// Asignar la función a una variable
val isEven: (Int) -> Boolean = modulo(2)
(1, 2, 3, 4).filter(isEven) // => [2, 4]
listOf(5, 6, 7, 8).filter(isEven) // => [6, 8] listOf
Un tipo función es un tipo que consta de una firma de función, es
decir, dos paréntesis que contiene la lista de parámetros (que son
opcionales) y un tipo de retorno. Ambas partes están separadas por el
operador '->'
.
Cuando se define un tipo función, siempre se debe indicar
explícitamente el tipo de retorno. Cuando se declaran funciones normales
que devuelven Unit
, se puede omitir el tipo de retorno ya
que el compilador lo infiere, pero no se puede omitir en los tipos
función. Además, debe poner los paréntesis para los parámetros, incluso
cuando el tipo función no acepta ningún parámetro.
fun executor(action:() -> Unit) {
()
action}
// 'action' es el nombre del parámetro y su tipo es '() -> Unit' que es una función.
// Por tanto el tipo de 'action' es un tipo función.
Ejemplo de un tipo función que no toma parámetros y devuelve
‘Unit’: () -> Unit
Ejemplo de un tipo función que no toma parámetros y devuelve un
String: () -> String
Ejemplo de un tipo función que toma un String y no devuelve nada:
(String) -> Unit
Ejemplo de un tipo función que toma dos parámetros y no devuelve
nada: (String, Float) -> Unit
Debido a que un tipo función es solo un tipo, significa que puede asignar una función a una variable, puede pasarla como un argumento a otra función o puede devolverla desde una función tal y como suceden en las `high-order functions’:
val saySomething: (String) -> Unit = { x -> println(x) }
("Good morning") // => Good morning saySomething
Una forma de instanciar una función tipo es usando el operador
'::'
. También podemos usar este operardor para pasar un
tipo función como parámetro de otra función especificando su nombre con
el operador y sin utilizar los paréntesis:
fun businessEmail(s: String): Boolean {
return s.contains("@") && s.contains("business.com")
}
(::businessEmail) // Invocar una 'high-order function' pasándole otra función por su nombre
isAnEmail
fun tell(text: String) {
(text)
println}
var saySomething: (String) -> Unit // La variable 'saySomething' es una variable de tipo función
= ::tell // instanciar el tipo función y asignarlo a la variable 'saySomething'
saySomething
("Hello") //=> Hello saySomething
En particular, una lambda es una función literal: una función anónima que no se declara pero se usa directamente como una expresión.
Básicamente, una lambda es un bloque de código que se puede pasar
como cualquier otro literal (por ejemplo, simplemente como una cadena
literal "una cadena"
). La combinación de estas
características permite a Kotlin soportar la programación funcional
básica.
En el ejemplo una variable ‘sum’ de tipo función y a la que le asignamos directamente una función ‘lambda’ con dos parámetros:
// Asignando una función 'lambda'
val sum: (Int, Int) -> Int = { x, y -> x + y }
(10, 20) // => 30
sum
// Equivalente usando el operador '::'
fun operation(x: Int, y: Int): Int {
return x + y
}
val sum: (Int, Int) -> Int = ::operation
(10, 20) // => 30 sum
En Kotlin, por convención si una función ‘lambda’ tiene solo
un parámetro, su declaración puede omitirse (junto con ->). El nombre
del único parámetro será 'it'
.
val isNegative: (Int) -> Boolean = { it < 0 } // este literal es del tipo '(it: Int) -> Boolean'
(-5) // => true isNegative
Otra convención es que si el último parámetro de una función acepta una función, una expresión ‘lambda’ que es pasada como el argumento correspondiente se puede colocar fuera de los paréntesis:
// lambda expression inside parentheses
val upperCaseLetters = "Hello World".filter({ it.isUpperCase() })
// lambda outside parentheses
val lowerCaseLetters = "Hello World".filter { it.isLowerCase() }
("$upperCaseLetters - $lowerCaseLetters") // => HW - elloorld println
El siguiente ejemplo tenemos una función de orden superior que acepta
una función lambda { (String) -> Boolean }
como
parámetro. Se expresa como “acepta una función ‘from String to
Boolean’”:
// El parámetro 'email' podemos usarlo como una función que acepta una cadena y devuelve un booleano.
fun isAnEmail(email: (String) -> Boolean) {
("myemail@example.com")
email}
({ s: String -> s.contains("@") }) // forma completa
isAnEmail{ s: String -> s.contains("@") } // Los paréntesis son opcionales
isAnEmail { it.contains("@") } // Uso de 'it' isAnEmail
Para parámetros no utilizados se utiliza el operador
'_'
:
val unusedSecondParam: (String, Int) -> Boolean = { s, _ ->
.length > 10
s}
("Hello World", 0) // 0 is unused unusedSecondParam
Una función anónima se parece mucho a una declaración de función normal, excepto que se omite su nombre. Su cuerpo puede ser una expresión o un bloque:
// Función anónima cuyo cuerpo es una expresión
fun(x: Int, y: Int): Int = x + y
// Función anónima con bloque
fun(x: Int, y: Int): Int {
return x + y
}
El tipo de los parámetros de una función anónima pueden omitirse si se pueden inferir por el contexto:
.filter(fun(item) = item > 0) ints
La inferencia de tipo de retorno para funciones anónimas funciona
igual que para las funciones normales: el tipo de retorno se deduce
automáticamente para funciones anónimas con un cuerpo de expresión y
debe especificarse explícitamente (o se supone que es
'Unit'
) para funciones anónimas con un cuerpo de
bloque.
Un ‘closure’ es una función que tiene acceso a variables y parámetros que se definen en un ámbito externo. A diferencia de Java, las variables ‘capturadas’ pueden ser modificadas.
fun printFilteredNamesByLength(length: Int) {
val names = arrayListOf("Adam", "Andrew", "Chike", "Kechi")
val filterResult = names.filter {
.length == length // 'length' se define fuera del ámbito de la lambda
it}
(filterResult)
println}
Para llevar más lejos la modularización de programas, Kotlin nos proporciona funciones locales, también conocidas como funciones anidadas o ‘nested functions’. Una función local es una función que se declara dentro de otra función.
Podemos hacer que nuestras funciones locales sean más concisas al no pasarles parámetros explícitamente. Esto es posible porque las funciones locales tienen acceso a todos los parámetros y variables de la función de cierre.
fun printCircumferenceAndArea(radius: Double): Unit {
fun calCircumference(radius: Double): Double = (2 * Math.PI) * radius
val circumference = "%.2f".format(calCircumference(radius))
fun calArea(radius: Double): Double = (Math.PI) * Math.pow(radius, 2.0)
val area = "%.2f".format(calArea(radius))
("The circle circumference of $radius radius is $circumference and area is $area")
print}
Las funciones marcadas con la palabra clave 'infix'
se
pueden llamar usando la notación ‘infix’ (omitiendo el punto y
los paréntesis para la llamada). Estas funciones deben cumplir los
siguientes requisitos:
Tienen que ser miembros de una clase o funciones de extensión
Deben tener un solo parámetro
Este parámetro no será 'vararg'
ni tener valor por
defecto
Para invocar una función 'infix'
en Kotlin no
necesitamos usar la notación de puntos ni los paréntesis. Hay que tener
en cuenta que las funciones 'infix'
siempre requieren que
se especifiquen tanto el receptor como el parámetro. Cuando se invoca un
método en el receptor actual, como por ejemplo dentro de la clase, se
necesita usar explicitamente la notación 'this'
. A
diferencia de las llamadas a métodos regulares, no se puede omitir.
class Student {
var kotlinScore = 0.0
infix fun addKotlinScore(score: Double): Unit {
this.kotlinScore = kotlinScore + score
}
fun build() {
this addKotlinScore 95.0 // Correcto
(95.0) // Correcto
addKotlinScore95.0 // Incorrectp: hay que especificar el receptor ('this')
addKotlinScore }
}
val student = Student()
95.00 // Invocando la función usando la notación 'infix'
student addKotlinScore .addKotlinScore(95) // Invocando la función con notación normal student
El compilador de Kotlin crea una clase anónima en versiones anteriores de Java cuando creamos o utilizamos expresiones lambda. Esto genera una sobrecarga, además de la carga de memoria que se genera cuando en una función lambda hace uso de variables de fuera de su entorno como en las ‘closures’.
Para evitar esta sobrecarga tenemos el modificador
'inline'
para las funciones. Una ‘High-Order
function’ con el modificador 'inline'
se integrará
durante la compilación del código. En otras palabras, el compilador
copiará la ‘lambda’ (o función literal) y también el cuerpo de la
función de orden superior y los pegará en el sitio de la llamada.
Con este mecanismo, nuestro código se ha optimizado
significativamente, no más creación de clases anónimas o asignaciones de
memoria extra. Por otro lado el uso de 'inline'
hace que el
compilador genere ficheros bytecode más grandes. Por esta razón, se
recomienda encarecidamente que solo se incluyan funciones de orden
superior más pequeñas que acepten lambda como parámetros.
Las clases son los bloques de construcción principales de cualquier
lenguaje de programación orientado a objetos. Las clases son
esencialmente tipos personalizados: un grupo de
variables y métodos unidos en una estructura coherente. Para definir una
clase se usa la palabra clave 'class'
.
class Invoice { ... }
La declaración de clase consiste en el nombre de la clase, el encabezado de la clase (especificando sus parámetros de tipo, el constructor primario, etc.) y el cuerpo de clase, rodeado de llaves. Tanto el encabezado como el cuerpo son opcionales. Si la clase no tiene cuerpo se pueden omitir las llaves.
Si no se especifica visibilidad, la visibilidad por defecto es
public
y por tanto cualquiera puede crear instancias de
dicha clase.
class Empty
En comparación con Java, puede definir varias clases dentro del mismo archivo fuente.
La clases pueden contener:
Constructores y bloques 'init'
Funciones
Propiedades
Clases anidadas e internas
Declaraciones de tipo 'object'
Una clase en Kotlin puede tener un constructor primario y uno o más constructores secundarios.
El constructor primario es parte del encabezado de la clase. Este constructor va después del nombre de la clase (y los parámetros de tipo que son opcionales). Por defecto, todos los constructores son públicos, lo que equivale efectivamente a que sean visible en todas partes donde la clase sea visible.
class Person constructor(firstName: String) { ... }
Si el constructor principal no tiene anotaciones o modificadores de
visibilidad, la palabra clave 'constructor'
se puede
omitir:
// Podemos omitir la palabra clave 'constructor'
class Person(firstName: String) { ... }
// Las anotaciones o modificadores de visibilidad requieren la palabra clave 'constructor'
class Customer public @Inject constructor(name: String) { ... }
Si una clase no-abstracta no declara ningún constructor (primario o secundario), tendrá un constructor primario sin argumentos generado automáticamente. La visibilidad del constructor será pública por defecto. Si no desea que su clase tenga un constructor público, es necesario declarar un constructor vacío con una visibilidad que no sea la predeterminada:
// Clase con un constructor privado
class DontCreateMe private constructor () { ... }
Para crear una instancia de una clase, se invoca al constructor como si de una función regular se tratase. En Kotlin no existe la palabra clave ‘new’:
class Person(val name: String) {
constructor(name: String, parent: Person) : this(name) {
.children.add(this)
parent}
}
val person = Person("John")
El constructor primario no puede contener ningún
código. El código de inicialización se puede colocar en bloques de
inicialización, que se definen con la palabra clave
'init'
.
Durante una inicialización de la instancia, los bloques de inicialización se ejecutan en el mismo orden en que aparecen en el cuerpo de la clase, intercalados con los inicializadores de propiedades:
class InitOrderDemo(name: String) {
val firstProperty = "First property: $name"
{
init ("First initializer block that prints ${name}")
println}
val secondProperty = "Second property: ${name.length}"
{
init ("Second initializer block that prints ${name.length}")
println}
}
Los bloques 'init'
pueden usarse para validar las
propiedades o parámetros mediante la palabra clave
'require'
:
class Person (val firstName: String, val lastName: String, val age: Int?) {
{
init(firstName.trim().length > 0) { "Invalid firstName argument." }
require(lastName.trim().length > 0) { "Invalid lastName argument." }
require
if (age != null) {
(age >= 0 && age < 150) { "Invalid age argument." }
require}
}
}
Tenga en cuenta que los parámetros del constructor primario se pueden usar en los bloques de inicialización. También pueden ser utilizados en los inicializadores de las propiedades en el cuerpo de la clase:
class Customer(name: String) {
// Uso del parámetro 'name' para inicializar la propiedad 'customerKey'
val customerKey = name.toUpperCase()
}
De hecho, para declarar propiedades e inicializarlas desde el constructor principal, Kotlin tiene una sintaxis concisa:
class Person(val firstName: String, val lastName: String, var age: Int) { ... }
De la misma forma que las propiedades definidas en el cuerpo de la
clase, las propiedades declaradas en el constructor primario pueden ser
mutables ('var'
) o de solo lectura
('val'
).
Cuando se usa el prefijo 'val'
Kotlin genera
automáticamente el método 'getter()'
y cuando se usa el
prefijo 'var'
Kotlin genera el 'getter()'
y
'setter()'
. Si no necesitamos los accesores se puede
definir el constructor sin los prefijos. De esta forma podemos definir
nuestros propios métodos accesores.
En este ejemplo, el constructor principal de la primera clase define las propiedades, mientras que el segundo no lo hace:
// class with primary constructor that defines properties
class Info (var name: String, var number: Int)
// class with primary constructor that does not define properties
class Info (name: String, number: Int)
La clase también puede declarar uno o varios constructores
secundarios, que se definen con la palabra clave
'constructor'
:
class Person {
// Constructor secundario
constructor(parent: Person) {
.children.add(this)
parent}
}
Si la clase tiene un constructor primario, cada constructor
secundario debe delegar en el constructor primario, ya sea
directamente o indirectamente a través de otro/s constructor/es
secundario/s. La delegación en otro constructor de la misma clase se
hace usando la palabra clave 'this'
:
class Person(val name: String) { // Constructor primario
// Constructor secundario
// Usamos 'this' para invocar al constructor primario
constructor(name: String, parent: Person) : this(name) {
.children.add(this)
parent}
}
Hay que tenera en cuenta que el código en los bloques de inicialización se convierte efectivamente en parte del constructor primario. La delegación en el constructor primario ocurre como la primera instrucción en el constructor secundario, por lo que el código en todos los bloques de inicialización se ejecuta antes que el constructor secundario. Incluso si la clase no tiene un constructor primario, la delegación todavía ocurre implícitamente y los bloques de inicialización aún se ejecutan antes:
class Constructors {
{
init ("Init block") // Se ejecuta antes que el constructor secundario
println}
constructor(i: Int) {
("Constructor")
println}
}
La diferencia importante entre los constructores secundarios y primarios es que los parámetros de los constructores primarios pueden definir propiedades, mientras que los parámetros de un constructor secundario siempre son solo parámetros.
Si los parámetros de un constructor primario también son propiedades, serán accesibles a lo largo de todo el ciclo de vida del objeto, al igual que las propiedades normales. Mientras que, si son simples parámetros, obviamente sólo son accesibles dentro del constructor, como cualquier otro parámetro de una función.
En Kotlin no se utiliza el concepto de ‘campo’ cuando hablamos de variables de instancia sino que se emplea el concepto de propiedades.
Las propiedades de una clase pueden declararse como mutables
(var
), o de inmutables o de sólo lectura
(val
):
class Address {
var name: String = ...
var street: String = ...
var city: String = ...
var state: String? = ...
var zip: String = ...
}
Para acceder a las propiedades de una clase usamos el operador punto
'.'
ya que a diferencia de Java no hay que utilizar
getters()
ni setters()
si hemos definido la
propiedad con 'val'
o 'var'
. Para usar la
propiedad, simplemente nos referimos a ella por su nombre, como si fuera
un campo en Java:
fun copyAddress(address: Address): Address {
val result = Address() // there's no 'new' keyword in Kotlin
.name = address.name // accessors are called
result.street = address.street
result// ...
return result
}
La sintaxis completa de definición de una propiedad en Kotlin:
{var|val} <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
El inicializador y las funciones 'getter()'
(y
'setter()'
si es una propiedad mutable) son opcionales. El
tipo de la propiedad es opcional si puede inferirse desde el
inicializador o desde el tipo de retorno del
'getter()'
.
var allByDefault: Int? // error: se requiere un inicializador explícito.
var initialized = 1 // propiedad de tipo Int, getter y setter por defecto
val simple: Int? // propiedad de tipo Int, getter por defecto, debe ser inicializada por el constructor
val inferredType = 1 // propiedad de tipo Int y getter por defecto
Si las funciones 'getter()'
(y 'setter()'
en propiedades mutables) por defecto no son suficientes se puede
codificar funciones 'getter()'
o 'setter()'
propias como cualquier otra función. Estas funciones están dentro de la
propiedad y por tanto tienen que ser identadas correctamente
val isEmpty: Boolean
get() = this.size == 0
var stringRepresentation: String
get() = this.toString()
set(value) {
(value) // parses the string and assigns values to other properties
setDataFromString}
Nótese que por convención, el nombre del parámetro de la función
'setter()'
es 'value'
pero no es obligatorio y
puede escogerse otro nombre.
Las propiedades pueden ser ‘private’, ‘protected’, o ‘public’ (visibilidad por defecto).
El campo de respaldo o ‘backing field’ es un campo generado automáticamente para cualquier propiedad que solo puede usarse dentro de los accesores (getter o setter).
Estará presente solo si utiliza la implementación predeterminada de
al menos uno de los accesores, o si un descriptor de acceso
personalizado lo hace referencia a través del identificador
'field'
. Este campo de respaldo se usa para evitar la
llamada recursiva y por tanto evitar un
‘StackOverflowError’.
Kotlin proporciona automáticamente este campo de respaldo. Se puede
hacer referencia a este campo en los accesores utilizando el
identificador 'field'
:
var counter = 0 // Note: the initializer assigns the backing field directly
set(value) {
if (value >= 0) field = value
}
Este campo es necesario ya que el siguiente código genera un
‘StackOverflowError’. Cuando Kotlin encuentra la propiedad
‘selectedColor’ llama al 'getter()'
correspondiente. Si
usamos ‘selectedColor’ dentro de la definición del propio
'getter()'
es cuando se producen llamadas recursivas que
acaban generando un desbordamiento de la pila. Kotlin provee del
‘backing field’ para evitarlo.
var selectedColor: Int = someDefaultValue
get() = selectedColor
set(value) {
this.selectedColor = value
()
doSomething}
// Código correcto
var selectedColor: Int = someDefaultValue
get() = field
set(value) {
= value
field ()
doSomething}
Las propiedades cuyo valor se conoce en el momento de la compilación
se pueden marcar como constantes de tiempo de compilación utilizando el
modificador 'const'
. Tales propiedades necesitan cumplir
los siguientes requisitos:
Top-level o miembros de un 'objet'
Inicializado con un valor de tipo String o un tipo primitivo
No tener un 'getter()'
propio
Estas propiedades pueden ser utilizadas en anotaciones:
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }
Normalmente, las propiedades declaradas con un tipo no nulo deben inicializarse en el constructor. Sin embargo, bastante a menudo esto no es conveniente. Por ejemplo, las propiedades se pueden inicializar mediante la inyección de dependencias, o en el método de configuración de una prueba de unidad. En este caso, no puede proporcionar un inicializador que no sea nulo en el constructor, pero aún así desea evitar las comprobaciones nulas al hacer referencia a la propiedad dentro del cuerpo de una clase.
Para manejar este caso, puede marcar la propiedad con el modificador
'lateinit'
:
public class MyTest {
lateinit var subject: TestSubject
@SetUp fun setup() {
= TestSubject()
subject }
@Test fun test() {
.method() // dereference directly
subject}
}
Para usar este modificador hay que cumplir ciertos requisitos:
Se puede usar únicamente en las propiedades 'var'
declaradas dentro del cuerpo de una clase. Por tanto no se puede usar en
propiedades declaradas en el constructor principal.
La propiedad no tiene un 'getter()'
o
'setter()'
personalizado.
Acceder a una propiedad antes de que haya sido inicializada lanzará una ‘UninitializedPropertyAccessException’.
Una función miembro es una función que se define dentro de una clase,
objeto o interfaz. Las funciones miembro se invocan con el operador
'.'
:
class Sample() {
fun foo() {
("Foo")
print}
}
().foo() // crea una instancia de 'Sample' e invoca el método 'foo' Sample
La herencia es fundamental para la programación orientada a objetos. Nos permite crear nuevas clases que reutilizan, amplían y/o modifican el comportamiento de los preexistentes. La clase preexistente se llama superclase (o clase base), y la clase nueva que estamos creando se llama clase derivada. Una clase derivada obtendrá implícitamente todos los campos, propiedades y métodos de la superclase (y de la superclase de la superclase si es el caso).
Hay una restricción en cuanto a cuántas clases podemos heredar; en una JVM, solo puede tener una clase base. Pero se puede heredar de múltiples interfaces.
La herencia es transitiva. Si la clase C se deriva de la clase B y esa clase B se deriva de una clase A dada, entonces la clase C es una clase derivada de A.
Todas las clases en Kotlin tienen una superclase común
'Any'
, que es la superclase predeterminada para una clase
sin supertipos declarados. Esta clase 'Any'
tiene unos
pocos métodos básicos como equals()
o
toString()
:
// Hereda de 'Any' implicitamente
class Example
Para declarar que una clase hereda de una clase base, colocamos el
tipo de la clase base después de dos puntos en el encabezado de la clase
derivada. Por defecto en Kotlin las clases están cerradas a la herencia,
es decir, son 'final'
. Para permitir que una clase sea
heredada, hay que utilizar la palabra clave 'open'
.
open class Base(p: Int)
// the derived class has a primary constructor
class DerivedWithConstructor(p: Int) : Base(p)
Si la clase derivada tiene un constructor primario, la clase base puede (y debe) inicializarse allí mismo, utilizando los parámetros del constructor primario.
Si la clase no tiene un constructor primario, entonces cada
constructor secundario tiene que inicializar el tipo base usando la
palabra clave 'super'
, o delegar a otro constructor que
haga eso. Tenga en cuenta que en este caso, diferentes constructores
secundarios pueden llamar a diferentes constructores de la clase
base:
open class Base(p: Int) {
constructor(p: Int, q: Int): this(p)
}
class DerivedWithoutConstructor : Base {
// calling the base constructor with super()
constructor(p: Int) : super(p)
}
Kotlin requiere anotaciones explícitas para la sobreescritura de funciones miembro.
Para que una función pueda ser sobreescrita se utiliza la palabra
clave 'open'
delante del nombre de la función. Dado que las
clases son finales en Kotlin, sólo podemos utilizar la
palabra clave 'open'
en funciones miembro de clases que
también hayan sido definidas como 'open'
.
Para indicar que una función en la clase derivada sobreescribe una
función de la clase padre se utiliza la palabra clave
'override'
delante del nombre de la función. De esta forma
le indicamos al compilador que esta función sobreescribe una función de
la clase padre y puede realizar las comprobaciones en tiempo de
compilación.
Una función con la palabra clave 'override'
también es
'open'
por definición y puede ser sobreescrita por las
subclases sucesivas. Es posible marcar una función
'override'
con la palabra clave 'final'
para
evitar que sea sobreescrita.
open class Base {
open fun v() { ... }
open fun x(p: Int) { ... }
fun nv() { ... }
}
class Derived: Base() {
override fun v() { ... }
final override fun x(p: Int) { ... } // Restringir la sobreescritura
}
En Kotlin, la herencia está regulada por la siguiente regla: si una
clase hereda varias implementaciones del mismo miembro de sus
superclases inmediatas, debe invalidar este miembro y proporcionar su
propia implementación. Para denotar el supertipo del cual se toma la
implementación heredada, usamos la palaba clave 'super'
calificado por el nombre de supertipo entre paréntesis angulares, por
ejemplo, super<Base>
:
open class A {
open fun f() { print("A") }
fun a() { print("a") }
}
interface B {
fun f() { print("B") } // interface members are 'open' by default
fun b() { print("b") }
}
class C() : A(), B {
// El compilador requiere que 'f()' sea sobreescrito para eliminar la ambigüedad
override fun f() {
super<A>.f() // call to A.f()
super<B>.f() // call to B.f()
}
}
La sobreescritura de propiedades funciona de manera similar a la sobreescritura de métodos.
Las propiedades declaradas en una superclase que luego se vuelven a
declarar en una clase derivada deben ir precedidas por la palabra clave
'override'
y deben tener un tipo compatible. También se
puede usar la palabra clave 'override'
como parte de la
declaración de una propiedad en un constructor primario.
Cada propiedad declarada puede ser sobreescrita por una propiedad con
un inicializador o por una propiedad con un método
'getter()'
open class Foo {
open val x: Int get() { ... }
}
class Bar : Foo() {
override val x: Int = ...
}
interface Foo1 {
val count: Int
}
class Bar1(override val count: Int) : Foo1
Durante la construcción de una nueva instancia de una clase derivada, la inicialización de la clase base se realiza como primer paso (precedida solo por la evaluación de los argumentos para el constructor de la clase base) y, por lo tanto, ocurre antes de que se ejecute la lógica de inicialización de la clase derivada.
Por lo tanto, durante la inicialización de las propiedades de la clase base las propiedades de la clase derivada aún no se han inicializado. Si alguna de esas propiedades se utilizan (de forma directa o indirecta) en la inicialización de la clase base se pueden producir comportamientos extraños o errores en tiempo de ejecución.
open class Base(val name: String) {
{
init ("Initializing Base")
println}
open val size: Int =
.length.also { println("Initializing size in Base: $it") }
name}
class Derived(name: String, val lastName: String) : Base(name.capitalize().also { println("Argument for Base: $it") }) {
{
init ("Initializing Derived")
println}
override val size: Int =
(super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}
// Argument for Base: Hello
// Initializing Base
// Initializing size in Base: 5
// Initializing Derived
// Initializing size in Derived: 10
El código en una clase derivada puede llamar a funciones en la
superclase e implementaciones de accesores de propiedades usando la
palabra clave 'super'
:
open class Foo {
open fun f() { println("Foo.f()") }
open val x: Int get() = 1
}
class Bar : Foo() {
override fun f() {
super.f() // Calling the super function
("Bar.f()")
println}
override val x: Int get() = super.x + 1
}
Kotlin admite clases abstractas al igual que Java.
Una clase abstracta es una clase con métodos marcados como abstractos y
que por tanto no puede ser instanciada. Si una clase tiene uno o varios
métodos abstractos es una clase abstracta y se indica con la palabra
clave 'abstract'
.
La subclase concreta de una clase abstracta deberá implementar todos los métodos y propiedades definidos en la clase abstracta; de lo contrario, también será considerada como una clase abstracta.
open class Person {
open fun fullName(): String { ... }
}
abstract class Employee (val firstName: String, val lastName: String): Person() {
// Variable de intancia en una clase abstracta
val propFoo: String = "bla bla"
abstract fun earnings(): Double
// Podemos tener métodos con implementación por defecto
override fun fullName(): String {
return lastName + " " + firstName;
}
}
Las clases abstractas pueden contener métodos con implementación por
defecto como cualquier otra clase. Las subclases de la clase abstracta
pueden sobreescribir la implementación predeterminada de un método pero
solo si el método tiene el modificador 'open'
. Los métodos
marcados como 'abstract'
también son 'open'
por defecto. Las clases abstractas también pueden definir variables de
instancia al contrario que pasa con las interfaces.
Las interfaces en Kotlin son muy similares a Java 8. Pueden contener declaraciones de métodos abstractos, así como implementaciones de métodos. Lo que los diferencia de las clases abstractas es que las interfaces no pueden almacenar el estado, es decir, no pueden tener variables de instancia. Pueden tener propiedades, pero estas deben ser abstractas o proporcionar implementaciones de accesores.
Una interfaz se define usando la palabra clave
'interface'
. Un método en una interfaz es abstracto por
defecto si no se proporciona una implementación.
interface MyInterface {
fun bar() // abstract by default
fun foo() {
// optional body
}
}
Una clase u objeto pueden implementar una o varias interfaces:
class Child : MyInterface {
override fun bar() {
// body
}
}
En una interfaz se pueden declarar propiedades. Una propiedad
declarada en una interfaz puede ser abstracta o puede proporcionar
implementaciones para el 'getter()'
o
'setter()'
. Las propiedades declaradas en interfaces no
pueden tener ‘backing fields’ y, por lo tanto, los accesores
declarados en interfaces no pueden hacer referencia a ellos.
interface MyInterface {
val prop: Int // abstract
val propertyWithImplementation: String
get() = "foo"
fun foo() {
(prop)
print}
}
class Child : MyInterface {
override val prop: Int = 29
}
Una interfaz puede derivar de otras interfaces y, por lo tanto, proporcionar implementaciones para sus miembros y declarar nuevas funciones y propiedades. Naturalmente, las clases que implementen dicha interfaz solo tienen que definir las implementaciones que faltan:
interface Named {
val name: String
}
interface Person : Named {
val firstName: String
val lastName: String
override val name: String get() = "$firstName $lastName"
}
data class Employee(
// implementing 'name' is not required
override val firstName: String,
override val lastName: String,
val position: Position
) : Person
En el caso de clases que hereden de varias interfaces, para evitar ambigüedades la subclase deberá proporcionar implementaciones tanto para métodos que tienen una implementación en una de las interfaces como en métodos que tiene implementaciones en varias interfaces.
interface A {
fun foo() { print("A") }
fun bar() // abstract
}
interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}
class C : A {
override fun bar() { print("bar") }
}
// la clase 'D' tieen que implementar tanto foo() como bar()
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
override fun bar() {
super<B>.bar()
}
}
Las clases, objetos, interfaces, constructores, funciones, propiedades y sus ‘setters’ pueden tener modificadores de visibilidad. (Los ‘setters’ siempre tienen la misma visibilidad que la propiedad).
Public - Este es el valor predeterminado, y se puede acceder a cualquier clase, función, propiedad, interfaz u objeto que tenga este modificador desde cualquier lugar.
Private - Se puede acceder a una función, interfaz o clase de nivel superior que se declara como privada solo dentro del mismo archivo.
Cualquier función o propiedad que se declare privada dentro de una clase, objeto o interfaz solo puede ser visible para otros miembros de esa misma clase, objeto o interfaz.
Un constructor privado debe usar la palabra clave
'constructor'
. Si un constructor es marcado como privado no
se puede instanciar un objeto con ese constructor.
class Car private constructor(val name: String, val plateNo: String) {
// ....
}
Protected - Solo se puede aplicar a propiedades o funciones dentro de una clase, objeto o interfaz, no se puede aplicar a funciones, clases o interfaces de nivel superior. Las propiedades o funciones con este modificador solo son accesibles dentro de la clase que lo define y cualquier subclase.
Internal - En un proyecto que tiene un módulo (módulo Gradle o Maven), una clase, objeto, interfaz o función especificada con este modificador dentro de ese módulo solo es accesible desde ese módulo.
Las Data classes son una forma concisa de crear
clases que solo contienen datos. Estas clases se definen con la palabra
clave 'data'
.
data class User(val name: String, val age: Int)
De forma automática el compilador crear los métodos
hashCode()
, equals()
, copy()
y
toString()
a partir de todas las propiedades declaradas en
el constructor primario. También se generan las funciones
componentN()
que corresponden a las propiedades declaradas
en orden en el constructor primario.
Para evitar comportamientos extraños estas clases deben cumplir ciertos requisitos:
El constructor primario necesita tener al menos un parámetro.
Todos los parámetros del constructor primario estarán marcados
como 'val'
o 'var'
.
Una ‘data class’ no puede ser 'abstract'
,
'open'
, 'sealed'
o
'inner'
.
(Antes de 1.1) Las ‘data classes’ no pueden extender de otras clases (pero pueden implementar interfaces).
El compilador sólo tiene en cuenta las propiedades declaradas en el constructor primario a la hora de generar los métodos de forma automática. Por tanto, para excluir propiedades se deben declarar en el cuerpo de la clase.
data class DataClassExample(val x: Int, val y: Int, val z: Int) {
// Propiedad excluida
var xx; Int = 0
}
val fooData = DataClassExample(1, 2, 4)
val fooCopy = fooData.copy(y = 100)
// El formato de 'toString()' es el mismo 'ClassName(prop=xx, prop=yy, ....)'
(fooData) // => DataClassExample(x=1, y=2, z=4)
println(fooCopy) // => DataClassExample(x=1, y=100, z=4) println
El compilador genera la función copy()
que permite
copiar un objeto y en caso necesario, crear la copia alterando algunas
de sus propiedades y manteniendo el resto.
data class User(val name: String, val age: Int)
// Función 'copy()' generada automáticamente
// fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
val jack = User(name = "Jack", age = 1)
// Copiamos el objeto pero modificando la propiedad 'age'
val olderJack = jack.copy(age = 2)
Las funciones componentN()
permite desestructurar las
propiedades:
val jane = User("Jane", 35)
val (name, age) = jane
("$name, $age years of age") // => Jane, 35 years of age println
Cada tipo se deriva de 'Any'
, que viene con una
declaración de método 'hashCode()'
. Esto es el equivalente
de un método 'hashCode()'
de clase ‘Object’ de
Java. Este método es importante cuando se insertan instancias del objeto
en colecciones, como un mapa. Al implementar este método, se debe
cumplir con una serie de requisitos:
Cuando se invoque en el mismo objeto más de una vez durante el
tiempo de ejecución, el método 'hashCode()'
debe devolver
constantemente el mismo valor, dado que el objeto no se
modificó.
Si para dos objetos el método 'equals()'
devuelve
true, entonces llamar al método 'hashCode()'
en cada uno de
ellos debería devolver el mismo valor entero.
Si dos objetos no son iguales, es decir, que el método
'equals()'
devuelve false cuando se comparan, no es un
requisito que cada método 'hashCode()'
del objeto devuelva
valores distintos. Sin embargo, producir un entero distinto para objetos
desiguales podría mejorar el rendimiento de las colecciones basadas en
‘hash’.
Las ‘data classes’ son un forma compacta y legible de
devolver dos o más valores de una función. Otra alternativa, menos
legible, es utilizar el tipo 'Pair'
o 'Triple'
proporcionado por Kotlin:
data class Result(val result: Int, val status: Boolean)
fun checkStatus() = Result(10, true) // función que retorna un tipo 'Result'
val (result, status) = checkStatus() // usamos la desestructuración de datos para acceder a los datos
En Kotlin una ‘sealed class’ es una clase abstracta (no se puede crear instancias) que otras clases pueden extender. Estas subclases se definen dentro del cuerpo de la ‘sealed class’, en el mismo archivo por lo que podemos conocer todas las subclases posibles simplemente viendo el archivo.
Las ‘sealed class’ se utilizan para representar jerarquías de clases restringidas, de forma que una clase solo pueda heredar de un conjunto limidado de tipos. Son, en cierto sentido, una extensión de las clases de enumeración.
Podemos agregar el modificador 'abstract'
, pero esto
es redundante porque estas clases son abstractas por defecto.
No pueden tener el modificador 'open'
ni
'final'
.
Podemos declarar clases de datos y objetos como subclases a una ‘sealed class’ (aún deben declararse en el mismo archivo).
No pueden tener constructores públicos ya que sus constructores son privados de forma predeterminada.
// shape.kt
sealed class Shape
class Circle : Shape()
class Triangle : Shape()
class Rectangle: Shape()
‘Covariance’ y ‘contravariance’ son términos que hacen referencia a la capacidad de usar un tipo más derivado (más específico) o menos derivado (menos específico) que el indicado originalmente. Los parámetros de tipo genérico admiten estos términos para proporcionar mayor flexibilidad a la hora de asignar y usar tipos genéricos. Cuando se hace referencia a un sistema de tipos, se definen como:
‘Covariance’ -> Permite usar un tipo
más derivado que el especificado originalmente. Puede asignar una
instancia de Class<Derived>
a una variable de tipo
Class<Base>
.
‘Contravariance’ -> Permite usar un
tipo más genérico (menos derivado) que el especificado originalmente.
Puede asignar una instancia de Class<Base>
a una
variable de tipo Class<Derived>
.
-* ‘Invariance’ -> Significa que solo se
puede usar el tipo especificado originalmente. Así, un parámetro de tipo
genérico invariable no es covariante ni contravariante. No se puede
asignar una instancia de List<Base>
a una variable de
tipo List<Derived>
o viceversa.
Al igual que en Java, en Kotlin las clases pueden tener tipos con parámetros.
class Box<T>(t: T) {
var value = t
}
En general, para crear una instancia de una clase genérica tenemos que proveer el tipo a la clase:
val box: Box<Int> = Box<Int>(1)
Si los parámetros se pueden inferir, como por ejemplo de los argumentos del constructor o por algún otro medio, se pueden omitir los argumentos de tipo:
val box = Box(1) // '1' tiene tipo Int así que el compilador infiere el tipo "Box<Int>"
Digamos que queremos crear una clase de productor que producirá un resultado de algún tipo ‘T’. A veces; queremos asignar ese valor producido a una referencia que es de un supertipo del tipo ‘T’.
Para lograr eso usando Kotlin, necesitamos usar la palabra clave
'out'
en el tipo genérico. Esto significa que podemos
asignar esta referencia a cualquiera de sus supertipos. El valor de
salida solo puede ser producido por la clase dada pero no consumido:
class ParameterizedProducer<out T>(private val value: T) {
fun get(): T {
return value
}
}
val a = ParameterizedProducer("string") // ParameterizedProducer<String>
val x: ParameterizedProducer<Any> = a // Correcto
val b = ParameterizedProducer(10) // ParameterizedProducer<Int>
val y: ParameterizedProducer<Number> = b // Correcto
val z: ParameterizedProducer<String> = b // ¡Error de compilación!
A veces, tenemos una situación opuesta, lo que significa que tenemos una referencia de tipo T y queremos poder asignarla al subtipo de T.
Podemos usar la palabra clave 'in'
en el tipo genérico
si queremos asignarlo a la referencia de su subtipo. La palabra clave
'in'
solo se puede utilizar en el tipo de parámetro que se
consume, no se produce:
class ParameterizedConsumer<in T> {
fun toString(value: T): String { // 'toString()' will only be consuming a value of type T.
return value.toString()
}
}
val a = ParameterizedConsumer<Number>()
val b: ParameterizedConsumer<Double> = a // Correcto
val c: ParameterizedConsumer<Int> = a // Correcto
val d: ParameterizedConsumer<String> = a // ¡Error de compilación!
Hay situaciones en las que no es importante el tipo específico de un
valor. Para ello usamos el operador '*'
o ‘star
projection’:
fun printArray(array: Array<*>) {
.forEach { println(it) }
array}
// Podemos pasar una matriz de cualquier tipo al método 'printArray()'
(arrayOf(1,2,3))
printArray
(arrayOf("hello", "World!!", 5)) printArray
Las funciones también pueden ser genéricas en los tipos que utilizan. Esto permite escribir una función que puede funcionar con cualquier tipo, en lugar de solo un tipo específico. Para ello, definimos los parámetros de tipo en la firma de función.
fun <T> choose(t1: T, t2: T, t3: T): T {
return when (Random().nextInt(3)) {
0 -> t1
1 -> t2
else -> t3
}
}
// Podemos usar esta función con enteros. Si el compilador puede inferir el tipo se puede omitir.
val r = choose<Int>(5, 7, 9)
val r = choose(5, 7, 9)
// También es válido usar la función con Strings
val s = choose<String>("BMW", "Audi", "Ford")
val s = choose("BMW", "Audi", "Ford")
El conjunto de todos los tipos posibles que pueden sustituirse por un parámetro de tipo dado puede estar restringido por restricciones genéricas.
El tipo más común de restricción es un límite superior que corresponde a la palabra clave de extensión de Java:
fun <T : Comparable<T>> sort(list: List<T>) { ... }
(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable<Int>
sort(listOf(HashMap<Int, String>())) // Error: HashMap<Int, String> is not a subtype of Comparable<HashMap<Int, String>> sort
El límite superior predeterminado (si no se especifica) es
'Any?'
.
Al igual que las funciones, Kotlin permite las clases internas, es decir, clases definidas dentro de otra clase. Son equivalentes a las clases internas estáticas en Java.
class OuterClass {
class NestedClass {
fun nestedClassFunc() { }
}
}
val nestedClass = OuterClass.NestedClass().nestedClassFunc()
Las clases internas, por otro lado, pueden hacer referencia a la
clase externa en la que se declaró. Para crear una clase interna,
colocamos la palabra clave 'inner'
antes de la palabra
clave 'class'
.
class OuterClass() {
val oCPropt: String = "Yo"
inner class InnerClass {
fun innerClassFunc() {
val outerClass = this@OuterClass
(outerClass.oCPropt)
print}
}
}
val demo = OuterClass().InnerClass().innerClassFunc() // => yo
Las clases de enumeración son similares a los tipos ‘enum’ de Java. El uso más básico de las clases de enumeración es la implementación de enumeraciones de tipos seguros. Cada constante de la enumeración es un objeto. Las constantes de la enumeración están separadas por comas.
enum class Country {
, France, Portugal
Spain}
Las enumeraciones pueden tener constructor:
enum class Direction(val angle: Int) {
(90), West(180), South(270), East(0)
North}
En Kotlin las constantes de la enumeración pueden declarar sus propias clases anónimas con sus métodos correspondientes, así como sobreescribir métodos primarios.
Si la enumeración define algún miembro, debe separar las definiciones de constantes de enumeración de las definiciones de miembros con un punto y coma, al igual que en Java.
enum class ProtocolState {
{
WAITING override fun signal() = TALKING
},
{
TALKING override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
En Kotlin las enumeraciones disponen de forma predeterminada de los métodos:
EnumClass.valueOf(value: String): EnumClass
->
Devuelve la constante de enumeración por su nombre. Lanza un
‘IllegalArgumentException’ si no existe la constante.
EnumClass.values(): Array<EnumClass>
->
Retorna un array con las constantes de enumeración.
Además de los métodos las instancias de enumeración vienen con dos
propiedades predefinidas. Uno es 'name'
de tipo ‘String’ y
el segundo es 'ordinal'
de tipo ‘Int’ para obtener la
posición de la constante dentro de la enumeración, teniendo en cuenta
que empiezan por 0:
enum class Country {
, France, Portugal
Spain}
(Country.Spain) // => Spain
println(Country.valueOf("Spain")) // => Spain
println
(Country.Portugal.name) // => Portugal
println(Country.France.ordinal) // => 1
println
fun countries() {
for (country in Country.values()) {
("Country: $country")
println}
}
Los objetos son muy similares a las clases. A veces necesitamos crear un objeto con una ligera modificación de alguna clase, sin declarar explícitamente una nueva subclase para ello. Java maneja este caso con clases internas anónimas. Kotlin generaliza ligeramente este concepto con ‘object expressions’ y ‘objects declarations’.
Estas son algunas de las características de los objetos en Kotlin:
Pueden tener propiedades, métodos y un bloque init.
Estas propiedades o métodos pueden tener modificadores de visibilidad.
No pueden tener constructores (primarios o secundarios).
Pueden extender otras clases o implementar una interfaz.
Hay importantes diferencias semánticas entre un ‘object expression’ y un ‘object declaration’:
Los ‘object expression’ se ejecutan (y se inicializan) inmediatamente, donde se usan.
Los ‘object declaration’ se inicializan cuando se accede por primera vez.
Por su parte, un ‘companion object’ se inicializa cuando se cargala clase correspondiente.
Para crear un objeto de una clase anónima que hereda de algún tipo (o tipos), escribimos:
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0
.addMouseListener(object : MouseAdapter() {
windowoverride fun mouseClicked(e: MouseEvent) {
++
clickCount}
override fun mouseEntered(e: MouseEvent) {
++
enterCount}
})
// ...
}
Colocamos la palabra clave 'object'
antes del nombre del
objeto que queremos crear. De hecho, estamos creando un
SINGLETON cuando creamos objetos en Kotlin usando esta
construcción ya que solo existe una instancia de un objeto.
object ObjectExample {
val baseUrl: String = "http://www.myapi.com/"
fun hello(): String {
return "Hello"
}
}
(ObjectExample.hello()) // => Hello
println
fun useObject() {
.hello() // => Hello
ObjectExampleval someRef: Any = ObjectExample // Usamos el nombre de los objetos tal como son
}
Al igual que una declaración de variable, una declaración de objeto no es una expresión y no se puede utilizar en el lado derecho de una declaración de asignación.
Los objetos en Kotlin pueden utilizarse también para crear constantes.
object APIConstants {
val baseUrl: String = "http://www.myapi.com/"
}
Los ‘companion objects’ son un tipo de ‘object
declaration’. Como Kotlin no admite clases, métodos o propiedades
estáticas como las que tenemos en Java, Kotlin provee los ‘companion
objects’. Estos objetos son básicamente un objeto que pertenece a
una clase que se conoce como la clase complementaria del objeto. Este
objeto se indica con la palabra clave 'companion'
.
Similar a los métodos estáticos en Java, un ‘companion object’ no está asociado con una instancia de clase, sino con la propia clase.
Se puede llamar a los miembros del ‘companion object’ usando simplemente el nombre de la clase como el calificador, como si fuera un método estático.
Un ‘companion object’ puede tener nombre que facilitará el ser invocado desde Java aunque es opcional.
class Person private constructor(var firstName: String, var lastName: String) {
// Podemos omitir el nombre del objeto
companion object {
var count: Int = 0
fun create(firstName: String, lastName: String): Person = Person(firstName, lastName)
// Podemos tener bloques 'init' dentro de un 'companion object'
{
init ("Person companion object created")
println}
}
}
val person = Person.create("John", "Doe")
class MyClass {
fun sayHello() = println("hello")
// Objeto con el nombre 'Factory' y que utilizaremos como 'Factory Pattern'
companion object Factory {
fun create(): MyClass = MyClass()
fun sayHelloFromCompanion() = MyClass().sayHello() // Podemos acceder a miembros de la clase
}
}
val myClass = MyClass.create()
().sayHello() // incorrecto
MyClass.Factory.sayHelloFromCompanion() // Invocar un método del 'companion' MyClass
Los objetos pueden ser desestructurados en múltiples variables. Esta sintaxis se llama declaración de desestructuración. Una declaración de desestructuración crea múltiples variables a la vez.
val (a, b, c) = fooCopy
("$a $b $c") // => 1 100 4 println
Desestructurando en un bucle 'for'
:
for ((a, b, c) in listOf(fooData)) {
("$a $b $c") // => 1 100 4
println}
val mapData = mapOf("a" to 1, "b" to 2)
// Map.Entry is destructurable as well
for ((key, value) in mapData) {
("$key -> $value")
println}
Kotlin proporciona su API de colecciones como una biblioteca estándar construida sobre la API de colecciones de Java como ‘ArrayList’, ‘Maps’, etc… Kotlin tiene dos variantes de colecciones: mutables e inmutables. Una colección mutable nos brinda la capacidad de modificar una colección ya sea agregando, eliminando o reemplazando un elemento. Las colecciones inmutables no se pueden modificar y no tienen estos métodos de ayuda.
Una lista es una colección ordenada de elementos. Esta es una colección popular ampliamente utilizada.
Podemos crear una lista inmutable usando la función
listOf()
. Los elementos no se pueden agregar ni
eliminar.
val fooList = listOf("a", "b", "c", 1, false)
val numbers: List<Int> = listOf(1, 2, 3, 4)
val emptyList: List<String> = emptyList<String>() // lista vacía
val nonNullsList: List<String> = listOfNotNull(2, 45, 2, null, 5, null) // lista de valores no nulos
(fooList.size) // => 3
println(fooList.first()) // => a
println(fooList.last()) // => c
println(fooList.indexOf("b")) // 1
println
// Se puede acceder a los elementos de una lista por su índice
(fooList[1]) // => b println
Se puede crear una lista mutable utilizando la
función mutableListOf()
:
val fooMutableList = mutableListOf("a", "b", "c")
.add("d")
fooMutableList(fooMutableList.last()) // => d
println(fooMutableList.size) // => 4 println
Con la función 'arrayListOf()'
crea una lista mutable y
devuelve un tipo ‘ArrayList’ de la API de colecciones de
Java.
Un conjunto o ‘set’ es una colección desordenada de elementos únicos. En otras palabras, es una colección que no admite duplicados.
Podemos crear un conjunto (o ‘set’) inmutable utilizando la función
'setOf()'
:
val fooSet = setOf("a", "b", "c")
(fooSet.contains("a")) // => true
println(fooSet.contains("z")) // => false println
Con la función 'mutableSetOf()'
podemos crear un
conjunto mutable:
// creates a mutable set of int types only
val intsMutableSet: MutableSet<Int> = mutableSetOf(3, 5, 6, 2, 0)
.add(8)
intsMutableSet.remove(3) intsMutableSet
La función 'hashSetOf()'
retorna un ‘HashSet’
de la API de colecciones de Java el cual almacena los elementos en una
tabla ‘hash’. Podemos añadir o quitar elementos de este conjunto porque
es mutable.
La función 'linkedSetOf()'
retorna un
‘LinkedHashSet’ de la API de colecciones de Java. También es un
conjunto mutable.
Los mapas asocian una clave a un valor. Las claves deben ser únicas, y por tanto no se permite duplicados. En cambio no hay obligación de que los valores asociados sean únicos. Cada clave sólo podrá asociarse a un solo elemento. De esa manera, cada clave se puede usar para identificar de forma única el valor asociado, ya que el mapa se asegura de que no pueda haber claves duplicadas en la colección. Los mapas implementan un forma eficiente de obtener el valor correspondiente a una determinada clave.
Podemos crear un mapa (‘map’) inmutable usando la
función 'mapOf()'
:
val fooMap = mapOf("a" to 8, "b" to 7, "c" to 9)
// Se puede acceder a los valores en el mapa por su clave
(fooMap["a"]) // => 8
println
// iterar por un mapa con un bucle 'for'
for ((key, value) in fooMap) {
("Key $key and value $value")
println}
La función 'linkedHashMap()'
retorna un
‘LinkedHasMap’ de la API de colecciones de Java, que es
mutable.
La función 'sortedMapOf()'
retorna un
‘SortedMap’ de la API de colecciones de Java que también es
mutable.
Las secuencias representan colecciones ‘lazily-evaluated’.
Podemos crear una secuencia utilizando la función
'generateSequence()'
. Las secuencias son excelentes cuando
el tamaño de la colección es desconocido a priori:
val fooSequence = generateSequence(1, { it + 1 })
val x = fooSequence.take(10).toList()
(x) // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
println
// An example of using a sequence to generate Fibonacci numbers:
fun fibonacciSequence(): Sequence<Long> {
var a = 0L
var b = 1L
fun next(): Long {
val result = a + b
= b
a = result
b return a
}
return generateSequence(::next)
}
val y = fibonacciSequence().take(10).toList()
(y) // => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] println
Kotlin proporciona ‘higher-order functions’ para trabajar con colecciones:
val z = (1..9).map { it * 3 }
.filter { it < 20 }
.groupBy { it % 2 == 0 }
.mapKeys { if (it.key) "even" else "odd" }
(z) // => {odd=[3, 9, 15], even=[6, 12, 18]} println
Un rango se define como un intervalo que tiene un valor de inicio y
un valor final. Los rangos son cerrados, lo que
significa que el valor inicial y final están incluidos en el rango. Los
rangos se crean con el operados ..
o con funciones como
rangeTo()
o downTo()
.
Para crear un intervalo sin incluir el último elemento usamos la
función until
.
val oneToNine = 1..9
val oneToFive: IntRange = 1.rangeTo(5)
val fiveToOne = 5.downTo(1)
(fiveToOne) // => 5 downTo 1 step 1
print
val oneToTen = (1..10).step(2).reversed() // => 9, 7, 5, 3, 1
("${tenToOne.first} - ${tenToOne.last}") // => 10 - 1
println
val oneToFour = 1.until(5)
(r) // => 1..4 print
Lost tipos IntRange
, LongRange
,
CharRange
tienen una característica extra y es que permite
iterar sobre los intervalos.
Una vez que se crea un intervalo, se puede usar el operador
in
para probar si un valor dado está incluido en el
intervalo o el operador !in
para comprobar si un valor no
está en el intervalo:
// Iterar con un bucle 'for'
for (i in 1..10) { // equivalent of 1 <= i && i <= 10
(i)
print}
// Iterar en sentido inverso
for (i in 4 downTo 1) {
(i)
print}
// Iterar por un intervalo sin incluir el último elemento
for (i in 1 until 10) {
// i in [1, 10), 10 is excluded
(i)
println}
// Pasos arbitrarios
for (i in 1..4 step 2) {
(i)
print}
for (i in 4 downTo 1 step 2) {
(i)
print}
Podemos verificar si un objeto es de un tipo en particular usando el
operador is
o si no es de un tipo con el operador
!is
.
Si un objeto pasa una verificación de tipo entonces se puede usar como ese tipo sin realizar la conversión explícitamente:
fun smartCastExample(x: Any): Boolean {
if (x is Boolean) {
// x is automatically cast to Boolean
return x
} else if (x is Int) {
// x is automatically cast to Int
return x > 0
} else if (x is String) {
// x is automatically cast to String
return x.isNotEmpty()
} else {
return false
}
}
(smartCastExample("Hello, world!")) // => true
println(smartCastExample("")) // => false
println(smartCastExample(5)) // => true
println(smartCastExample(0)) // => false
println(smartCastExample(true)) // => true println
La conversión inteligente (‘smart cast’) también funciona
con bloques when
o bucles while
:
fun smartCastWhenExample(x: Any) = when (x) {
is Boolean -> x
is Int -> x > 0
is String -> x.isNotEmpty()
else -> false
}
Podemos usar el operador as
(o el operador de
conversión no segura o ‘unsafe cast operator’)
para convertir explícitamente una referencia de un tipo a otro tipo en
Kotlin. Si la operación de conversión explícita es ilegal, tenga en
cuenta que se lanzará una excepción de tipo
‘ClassCastException’.
Para evitar que se lance una excepción al realizar la conversión,
podemos usar el operador de conversión seguro
as?
. Este operador intentará la conversión y si no se puede
realizar la conversión devolverá 'null'
en vez de lanzar la
excepción. Por tanto la variable que contiene el resultado de una
conversión segura debe ser capaz de mantener un resultado nulo:
val circle = shape as Circle
val circle: Circle? = shape as? Circle // Conversión segura
Para que una variable contenga el valor ‘null’ debe
especificarse explícitamente como ‘nullable’. Una variable se
puede especificar como ‘nullable’ agregando un ?
a
su tipo.
Podemos acceder a una variable o método ‘nullable’
utilizando el operador '?.'
también llamado ‘Safe Call
Operator’. Un método o variable sólo será invocado si tiene una
valor no nulo. En caso de que sea nulo será ignorado evitando un
‘NullPointerException’
Kotlin provee el operador '?:'
, también llamado
‘Elvis Operator’ para especificar un valor alternativo para
usar si una variable es nula. Cuando la expresión de la izquierda del
operador '?:'
no es nulo entonces lo devuelve. En caso de
que sea nulo devuelve la expresión de la derecha. La expresión de la
derecha sólo será evaluada si la expresión de la izquierda es
‘null’.
val name: String = null // no compilará ya que no puede contener valores nulos
var fooNullable: String? = "abc"
?.length // => 3
fooNullable
// 'Elvis Operator'
?.length ?: -1 // => 3
fooNullable
= null
fooNullable val len: Int? = fooNullable?.length // El tipo de retorno de 'fooNullable' puede ser 'null' y por tanto debemos usar Int?
?.length // => null
fooNullable?.length ?: -1 // => -1
fooNullable
// Encadenar 'safe calls'. La cadena retorna 'null' si alguna de ellas es 'null'
fun getCountryNameSafe(person: Person?): String? {
return person?.address?.city?.country?.name
}
// Dado que 'throw' y 'return' son expresiones en Kotlin se pueden usar en la parte derecha del operador 'Elvis'
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
De manera similar, podemos devolver tipos ‘nullable’ y no ‘nullable’ desde una función.
fun getName(): String? = name // Esta función puede o no devolver una referencia nula.
fun getNotNullName(): String = name ?: "John" // Esta función no devolverá una referencia nula
() // => null
getName() // => John getNotNullName
Con ‘smart cast’, el compilador rastrea las condiciones
dentro de una expresión 'if'
. Si realizamos la verificación
de que una variable no es nula, entonces el compilador nos permitirá
acceder a la variable como si hubiera sido declarada como un tipo no
anulable:
var l = if (name != null) name.length else -1
El operador de aserción no-nulo '!!'
convierte cualquier
valor a un tipo no nulo y lanza una excepción
‘NullPointerException’ si el valor es nulo.
val length: Int = name!!.length
En Kotlin hay tenemos la igualdad estructural y la igualdad referencial.
La igualdad estructural se comprueba con la operación
'=='
y la parte contraria '!='
y se utiliza
para comprobar si dos valores o variables son iguales
(equals()
)
if (a == b) {
// ...
} else {
// ...
}
La igualdad referencial se comprueba con la operación
'==='
y su contraparte '!=='
y evalúa a
true
si y sólo si dos referencias apuntan al mismo
objeto.
Son funciones que proporciona Kotlin para aumentar la biblioteca estándar de Java.
'apply'
es una función de extensión de la biblioteca
estándar de Kotlin declarada en 'Any'
, por lo que puede ser
invocada en cualquier tipo de instancia. 'apply'
acepta una
expresión lambda que es invocada y el receptor es la instancia donde es
llamada. La función 'apply'
devuelve una instacia del
original.
Su uso principal es hacer que el código que necesita inicializar una instancia sea más legible permitiendo que las funciones y las propiedades se llamen directamente dentro de la función antes de devolver el valor en sí.
data class Person(var firstName: String, var lastName : String)
var person = Person("John", "Doe")
{ this.firstName = "Bruce" }
person.apply (person) // => Person(firstName=Bruce, lastName=Doe)
print
// 'apply' retorna la instancia original.
.apply { this.firstName = "Bruce" }.firstName = "Steve"
person(person) // => Person(firstName=Steve, lastName=Doe) print
La función 'let'
toma el objeto sobre el que se invoca
como parámetro y devuelve el resultado de la expresión lambda. Es útil
cuando desea ejecutar algún código en un objeto antes de devolver algún
valor diferente y no necesita mantener una referencia al original:
fun main(args: Array<String>) {
var str = "Hello World"
.let { println("$it!!") } // => Hello World!!
str(str) // => Hello World
println}
var strLength = str.let { "$it function".length } // devuelve el resultado de la expresión lambda
("strLength is $strLength") // => strLength is 25 println
La función 'with'
es una función de nivel superior
diseñada para los casos en los que desea llamar a múltiples funciones en
un objeto y no desea repetir el receptor cada vez. La función
'with'
acepta un receptor y un cierre para operar en dicho
receptor:
data class Person(var firstName: String, var lastName : String)
var person = Person("John", "Doe")
(person)
with{
= "Bruce"
firstName = "Doe"
lastName }
// notación sin 'with'
.firstName = "John"
person.lastName = "Doe" person
La última expresión en un bloque 'with'
se retorna como
resultado:
var name = with(person)
{
= "John"
firstName = "Doe"
lastName "$firstName $lastName" // se retorna este valor y se almacena en 'name'
}
(name) // => John Doe println
'Run'
es una función que combina las características de
'with'
y 'let'
. Esto significa que se pasa una
expresión lambda a la función 'run'
y la instancia del
objeto es el receptor. El valor de retorno de la expresión lambda se usa
como valor de retorno:
.run {
personthis.firstName = "Bruce"
}
(person) // => Person(firstName=Bruce, lastName=Doe) print
La diferencia clave entre 'let'
y 'run'
es
que con 'run'
el receptor es la instancia, mientras que en
'let'
, el argumento de la expresión lambda es la
instancia.
Esta función acepta un entero y una función literal. La función literal será invocada las veces indicadas por el valor entero.
(10, { println("Hello") }) repeat
La función 'lazy'
es una función cuya utilidad es
envolver funciones costosas en términos de rendimiento o de recursos y
que serán invocadas cuando sean requeridas por primera vez. La ventaja
de utilizar esta función proporcionada por la biblioteca estándar de
Kotlin es que el compilador mantendrá la invocación sincronizada
evitando que sea invocada más de una vez.
fun readStringFromDatabase(): String = ... // expensive operation
val lazyString = lazy { readStringFromDatabase() }
La función 'use'
es similar a la declaración
'try-with-resources'
presente en Java 7. La función
'use'
se define como una función de extensión de la
interfaz ‘Closeable’. Ejecuta la función y luego ‘cierra’ el
recurso de forma segura.
Kotlin proporciona un conjunto de funciones que nos permiten agregar una cantidad limitada de especificaciones formales a nuestro código. Una especificación formal es una aserción que siempre debe ser verdadera o falsa en la ubicación cuando se ejecuta la aserción. Estos también se conocen como contratos o diseño por contrato:
'require()'
y 'requireNotNull()'
lanza
una excepción de tipo ‘IllegalArgumentException’ y se utiliza
para garantizar que los argumentos cumplan el contrato.
'assert()'
lanza una excepción
‘AssertionException’ y se utiliza para garantizar que nuestro
estado interno es consistente.
'check()'
y 'error()'
lanza una
excepción ‘IllegalStateException’ y también se usa para
mantener la consistencia del estado interno.
Estas funciones son similares. La clave que las diferencia es el tipo de excepción que se plantea.
fun neverEmpty(str: String) {
(str.length > 0, { "String should not be empty" })
require(str)
println}
fun foo(k: Int, value: Boolean) {
(k > 10, { "k should be greater than 10" }) // => throws an IllegalArgumentException
require(k) // => throws an IllegalArgumentException if the value is null.
requireNotNull(value) // => throws an IllegalStateException if the value is false
checkif (k == 20) error("Error: k == 20") // => throws an IllegalStateException
}
En Kotlin todas las excepciones son subclases de la clase
'Throwable'
. Cada excepción tiene un mensaje, un
seguimiento de la pila y una causa opcional. Kotlin no tiene
‘checked exceptions’ a diferencia de Java, que realiza la
distinción entre tipos de excepciones.
Para lanzar un objeto de excepción, se utiliza la palabra clave
'throw'
:
throw Exception("Message")
Para capturar una excepción lanzada se utiliza un bloque
'try'
:
try {
// some code
}
catch (e: SomeException) {
// handler
}
finally {
// optional finally block
}
Puede haber 0 o más bloques 'catch'
. Los bloques
'finally'
son opcionales y puede omitirse. Sin embargo,
tiene que haber al menos un bloque 'catch'
o
'finally'
.
Al igual que muchas otras instrucciones en Kotlin, 'try'
es una expresión y por tanto puede devolver un valor:
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
El valor devuelto por un 'try'
que actúa como expresión
es la última expresión en el bloque 'try'
o la última
expresión en el bloque 'catch'
. El contenido del bloque
'finally'
no afecta al resultado de la expresión.
'throw'
es una expresión en Kotlin, así que se puede
usar, por ejemplo, como parte de una ‘Elvis expression’:
val s = person.name ?: throw IllegalArgumentException("Name required")
El tipo de retorno de una expresión 'throw'
es el tipo
especial 'Nothing'
. Este tipo no tiene valores y se utiliza
para marcar ubicaciones del código que nunca se pueden alcanzar.
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
Cuando llame a la función del ejemplo anterior, el compilador sabrá que la ejecución no continúa más allá de la llamada:
val s = person.name ?: fail("Name required")
(s) // 's' is known to be initialized at this point println
Otro caso en el que puede encontrar este tipo es la inferencia de
tipos. La variante ‘nullable’ de este tipo,
'Nothing?'
, tiene exactamente un valor posible, que es el
valor 'null'
. Si se usa el valor nulo para inicializar un
valor de un tipo inferido y no hay otra información que se pueda usar
para determinar un tipo más específico, el compilador inferirá el tipo
'Nothing?'
:
val x = null // 'x' tiene el tipo `Nothing?`
val l = listOf(null) // 'l' tiene el tipo `List<Nothing?>
Las anotaciones permiten a los desarrolladores agregar un significado adicional a las clases, interfaces, parámetros, etc., en el momento de la compilación. Las anotaciones pueden ser utilizadas por el compilador o por su propio código a través de la reflexión en tiempo de ejecución. Dependiendo del valor de la anotación, el significado del programa o los datos puede cambiar.
Kotlin representa funciones de nivel de paquete (funciones fuera de
una clase) como métodos estáticos. Kotlin también puede generar métodos
estáticos para funciones definidas en
‘objects’ y ‘companin
objects’ si anota esas funciones como
'@JvmStatic'
. Si usa esta anotación, el compilador generará
tanto un método estático en la clase envolvente del objeto como un
método de instancia en el propio objeto.
class C {
companion object {
@JvmStatic fun foo() {}
fun bar() {}
}
}
// Ahora 'foo()' es estático en Java pero no 'bar()'
.foo(); // correcto
C.bar(); // error: 'bar()' no es un método estático
C.Companion.foo(); // correcto
C.Companion.bar(); // la única forma de invocar a 'bar()'
C
object Obj {
@JvmStatic fun foo() {}
fun bar() {}
}
// In Java:
.foo(); // correcto
Obj.bar(); // error
Obj.INSTANCE.bar(); // correcto, una llamada a través de la instancia 'Singleton'
Obj.INSTANCE.foo(); // correcto Obj
Dado que todas las excepciones en Kotlin son ‘unchecked
exceptions’, no es necesario agregar una lista de posibles
excepciones a las firmas de métodos como las que hay en Java. Sin
embargo, es posible que deseamos informar a los usuarios de Java que
nuestra API produce excepciones en ciertas situaciones. Podemos hacer
esto utilizando la anotación '@Throws'
, que se utiliza para
indicar al compilador que genere cláusulas de lanzamiento en los métodos
generados.
@Throws(FileNotFoundException::class)
fun fileExists(path: String) {
// ...
}
Dada una función con parámetros por defecto,
'@JvmOverloads'
hará que el compilador cree múltiples
métodos sobrecargados para cada parámetro predeterminado.
Podemos cambiar el nombre del fichero creado por Kotlin con la
anotación '@JvmName'
:
// example.kt (sin @JvmName)
package demo
class Foo
fun bar() { ... }
// En Java
.Foo();
new demo.ExampleKt.bar();
demo
// Usamos la anotación '@JvmName' al principio del fichero para indicar al compilador el nombre del fichero
@file:JvmName("DemoUtils")
package demo
class Foo
fun bar() { ... }
// Ahora en Java
.Foo();
new demo.DemoUtils.bar(); demo
Además de indicarle al compilador el nombre del fichero con
'@JvmName'
podemos indicarle que combine todas las
funciones de nivel superior de varios ficheros en Kotlin en una única
clase Java con la anotación '@JvmMultifileClass'
.
Reflection es el nombre dado a la inspección del código en tiempo de ejecución en lugar de tiempo de compilación. Puede usarse para crear instancias de clases, buscar funciones e invocarlas, inspeccionar anotaciones, buscar campos y descubrir parámetros y genéricos, todo sin conocer esos detalles en el momento de la compilación.
Por ejemplo, si necesitamos persistir tipos en una base de datos y a priori no conocemos el tipo de datos podemos utilizar la reflexión para conocer el tipo de datos en tiempo de ejecución y crear la SQL apropiada a ese tipo.
Para usar la reflexión en Kotlin hay que importar el paquete
kotlin.reflect
.
'KClass'
es el tipo central utilizado en la reflexión de
Kotlin. Cada tipo tiene una instancia de 'KClass'
en tiempo
de ejecución que contiene detalles de las funciones, propiedades,
anotaciones, etc., para ese tipo. Para obtener una instancia de
'KClass'
para cualquier tipo, usamos la sintaxis especial
'::class'
en una instancia de ese tipo:
val name = "George"
val kclass = name::class // => class kotlin.String
data class Person(val firstName: String, val lastName: String)
(Person::class.qualifiedName) // => Person
println(Person::class.isData) // => true println
Podemos obtener una referencia a la clase utilizando el ‘fully qualified name or FQN’ de la clase y la API ‘reflection’ de Java. Si el compilador no encuentra la clase lanza una ‘ClassNotFoundException’:
package com.example
data class Person(val firstName: String, val lastName: String)
val kClass = Class.forName("com.example.Person").kotlin // => class com.example.Personal
Para crear instancias de tipo sin conocer el tipo en tiempo de
ejecución podemos invocar la función 'createInstance()'
en
una referencia de 'KClass'
. Podemos usar esta función con
clases sin parámetros o con parámetros opcionales, es decir, que tengan
valor por defecto:
class PositiveInteger(value: Int = 0)
fun createInteger(kclass: KClass<PositiveInteger>): PositiveInteger {
return kclass.createInstance()
}
Podemos devolver una lista de todos los constructores declarados en
un tipo dado usando la propiedad 'constructor'
disponible
en el tipo 'KClass'
. Podemos instanciar una clase usando el
constructor con la instrucción 'call'
o
'callBy'
:
class Person constructor(val firstName: String, val lastName: String)
fun <T : Any> printConstructors(kclass: KClass<T>) {
.constructors.forEach {
kclass(it.parameters)
println}
}
(Person::class) // Muestra el/los constructor/es de la clase 'Person'
printConstructors
// Recupera el primer constructor. Si no encuentra ninguno lanza una excepción.
val constructor = Person::class.constructors.first()
val person = constructor.call("John", "Doe") // Invocar al constructor con 'call'
(person.firstName) // => John println
Además de los constructores de una clase, también podemos acceder y
listar las funciones de una clase con la propiedad
'functions'
disponible en el tipo
'KClass'
:
class Person constructor(val firstName: String, val lastName: String) {
fun getName(): String {
return "$firstName $lastName"
}
}
fun <T : Any> printFunctions(kclass: KClass<T>) {
.functions.forEach {
kclass(it.name)
println}
}
(Person::class) // => getName equals hashCode toString
printFunctions
val function = Person::class.functions.find { it.name == "getName" }
val person = Person("John", "Doe")
?.call(person) // => John Doe function
(todo)
KotlinTest es el framework para probar y testear el código en Kotlin.
Añadir la dependencia a Gradle:
testCompile 'io.kotlintest:kotlintest:x.y.z'
.
Normalmente, para mantener ordenada la estructura del proyecto los
ficheros de test se ubican en src/test/kotlin
Una especificación o ‘spec’ es simplemente la manera en que las pruebas se presentan en los archivos de clase. Hay varias especificaciones diferentes disponibles como FunSpec, **StringSpec+*, ShouldSpec. etc…
La especificación FunSpec permite crear pruebas similares al estilo jUnit. Para escribir un test unitario invocamos la función ‘test’ que toma dos parámetros. El primer parámetro es una descripción de la prueba unitaria y el segundo es una función literal que contiene el cuerpo de la prueba. La descripción o nombre de la prueba aparecerá en la salida, así que permite saber que prueba/s han pasado la prueba y cuáles han fallado.
class StringTestWithFunSpec : FunSpec() {
{
init ("String.startsWith should be true for a prefix") {
test"helloworld".startsWith("hello") shouldBe true
}
("String.endsWith should be true for a prefix") {
test"helloworld".endsWith("world") shouldBe true
}
}
}
La especificación StringSpec es la especificación recomendada por los autores de Kotlin y es la especificación más simple y compacta ya que reduce la sintaxis al mínimo. Se escribe una cadena seguida de una expresión lambda para probar el código:
class StringTestWithStringSpec : StringSpec() {
{
init "strings.length should return size of string" {
"hello".length shouldBe 5
"hello" shouldBe haveLength(5)
}
}
}
La especificación ShouldSpec es similar a
FunSpec pero usa la palabra clave 'should'
en vez de 'test'
:
class StringTestWithShouldSpec : ShouldSpec() {
{
init ("return the length of the string") {
should"sammy".length shouldBe 5
"".length shouldBe 0
}
// Nested form
"String.length" {
("return the length of the string") {
should"sammy".length shouldBe 5
"".length shouldBe 0
}
}
}
}
La especificación WordSpec usa también la palabra
clave 'should'
. Esta especificación permite anidar las
pruebas:
class StringTestWithWordSpec : WordSpec() {
{
init "String.length" should {
"return the length of the string" {
"sammy".length shouldBe 5
"".length shouldBe 0
}
}
}
}
La especificación BehaviorSpec utiliza las palabras
clave 'given'
, 'when'
y 'then'
para crear pruebas unitarias más cercanas al lenguaje natural:
class StringTestWithBehaviorSpec : BehaviorSpec() {
{
init ("a stack") {
givenval stack = Stack<String>()
("an item is pushed") {
`when`.push("kotlin")
stack("the stack should not be empty") {
then.isEmpty() shouldBe true
stack}
}
("the stack is popped") {
`when`.pop()
stack("it should be empty") {
then.isEmpty() shouldBe false
stack}
}
}
}
}
La especificación FeatureSpec es similar a la
especificación BehaviorSpec pero utiliza las palabras
clave 'feature'
y 'scenario'
:
class StringTestWithFeatureSpec : FeatureSpec() {
{
init ("Hello World") {
feature("should starts with 'Hello'") {
scenario"Hello World".startsWith("Hello")
}
("should ends with 'World'") {
scenario"Hello World".endsWith("World")
}
}
}
}
Los matchers prueban alguna propiedad, indicada por el nombre del matcher, más allá de la simple igualdad. Por ejemplo, un comparador puede verificar si una cadena está vacía o si un entero es positivo.
// [String matchers]
class StringTestWithDifferentMatchers : StringSpec() {
{
init "Tests string prefixes" {
"Hello".startsWith("He") shouldBe true
"Hello" shouldBe startWith("He")
}
"Tests substrings"{
"Hello" shouldBe include("el")
}
"Test string suffixes" {
"Hello".endsWith("llo") shouldBe true
"Hello" shouldBe endWith("llo")
}
"Tests the length of a string" {
"Hello".length shouldBe 5
"Hello" shouldBe haveLength(5)
}
"Tests the equality using a regular expression" {
"Hello" shouldBe match("He...")
}
}
}
// [Collection matchers]
class CollectionTestWithDifferentMatchers : StringSpec() {
private val listWithDifferentIntegers = listOf(1, 2, 3, 4, 5)
private val mapWithKeyAndValues = mapOf<Int, String>(1 to "Hello", 2 to "World")
{
init "Tests that a collection should contain the given element" {
(3)
listWithDifferentIntegers shouldBe contain}
"Test the size of the collection" {
<Int>(5)
listWithDifferentIntegers shouldBe haveSize}
"Tests that the collections should be sorted" {
<Int>()
listWithDifferentIntegers shouldBe sorted}
"Tests that the collection has a single element that is equal to the given element" {
(2)
listWithDifferentIntegers shouldNotBe singleElement}
"Tests that the collection contains all the given elements. The order of these elements does not matter." {
(1, 2, 4)
listWithDifferentIntegers shouldBe containsAll}
"Tests whether the collection is empty or not" {
<Int>()
listWithDifferentIntegers shouldNotBe beEmpty}
"Tests whether the map contains mapping from a key to any value" {
(2)
mapWithKeyAndValues shouldBe haveKey}
"Tests whether the map contains the value for at least one key" {
("Hello")
mapWithKeyAndValues shouldBe haveValue}
"Tests that the map contains the exact mapping of the key to the value" {
(2, "World")
mapWithKeyAndValues shouldBe contain}
}
}
// [Floating point matchers]
// En valores en punto flotante más que la igualdad absoluta se utiliza la 'tolerancia' que es el valor mínimo entre dos valores que satisfacen el criterio de igualdad
class FloatNumberTestWithTolerance : StringSpec() {
private val randomDouble = 18.005
private val enoughDouble = 18.006
{
init "Test if two numbers are equals" {
(enoughDouble)
randomDouble shouldNotBe equals(enoughDouble plusOrMinus 0.01)
randomDouble shouldBe }
}
}
// [Exception matchers]
// 'shouldThrow fallará si se lanza una excepción diferente
class ExceptionTest : StringSpec() {
{
init "Testing IllegalArgumentException" {
<IllegalArgumentException> {
shouldThrow(10.0) shouldEqual 10.5
addNumberToTwo}
}
}
}
@Throws(IllegalArgumentException::class)
fun addNumberToTwo(a: Any): Int {
if (a !is Int) {
throw IllegalArgumentException("Number must be an integer")
}
return 2 + a
}
Los matchers se pueden combinar usando los
operadores de la lógica booleana como 'and'
y
'or'
:
class CombiningMatchers : StringSpec() {
{
init "Combining matchers" {
"Hello World" should (startWith("Hel") and endWith("rld"))
}
}
}
Un inspector en KotlinTest es la forma más fácil de probar el contenido de ‘collections’:
val kings = listOf("Stephen I", "Henry I", "Henry II", "Henry III", "William I", "William III")
class InspectorTests : StringSpec() {
{
init "all kings should have a regal number" {
(kings) {
forAll("I")
it should endWith}
}
"only one king has the name Stephen" {
(kings) {
forOne("Stephen")
it should startWith}
}
"some kings have regal number II" {
(kings) {
forSome("II")
it should endWith}
}
"at least one King has the name Henry" {
(kings) {
forAtLeastOne("Henry")
it should startWith}
}
}
}
A veces es posible que sea necesario ejecutar algo de código, antes
de que se ejecuten las pruebas o después de que se completen todas las
pruebas (sean exitosas o no). Esto se puede lograr mediante el uso de la
clase abstracta 'ProjectConfig'
. Para usar esto,
simplemente se crea un objeto que extienda de esta clase abstracta y
asegurarse que esté en la ruta de la clase. KotlinTest lo encontrará
automáticamente y lo invocará:
object codeExecutionBeforeAndAfterTestCases : ProjectConfig() {
override fun beforeAll() {
// ...code
}
override fun afterAll() {
// ...code
}
}
Kotlin está diseñado teniendo en cuenta la interoperabilidad de Java. El código Java existente puede llamarse desde Kotlin de una manera natural, y el código Kotlin también se puede usar desde Java sin problemas.
Casi todo el código de Java se puede utilizar sin problemas:
import java.util.*
fun demo(source: List<Int>) {
val list = ArrayList<Int>()
// 'for'-loops work for Java collections:
for (item in source) {
.add(item)
list}
// Operator conventions work as well:
for (i in 0..source.size - 1) {
[i] = source[i] // get and set are called
list}
}
Los métodos que siguen las convenciones de Java para ‘getters’ y ‘setters’ (métodos sin argumentos con nombres que comienzan con ‘get’ y métodos con argumentos únicos con nombres que comienzan con ‘set’) se representan como propiedades en Kotlin.
Los métodos de acceso booleanos (donde el nombre del ‘getter’ comienza con ‘is’ y el nombre del ‘setter’ comienza con ‘set’) se representan como propiedades que tienen el mismo nombre que el método ‘getter’:
import java.util.Calendar
fun calendarDemo() {
val calendar = Calendar.getInstance()
if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // call getFirstDayOfWeek()
.firstDayOfWeek = Calendar.MONDAY // call setFirstDayOfWeek()
calendar}
if (!calendar.isLenient) { // call isLenient()
.isLenient = true // call setLenient()
calendar}
}
Si la clase Java solo tiene un ‘setter’, no será visible como una propiedad en Kotlin, ya que Kotlin no admite propiedades que tengan únicamente el método ‘setter’.
Si un método Java devuelve 'void'
, devolverá
'Unit'
cuando se llame desde Kotlin. Si, por casualidad,
alguien usa ese valor de retorno, el compilador de Kotlin lo asignará en
el sitio de la llamada, ya que el valor en sí mismo se conoce de
antemano (es 'Unit'
).
Algunas de las palabras clave de Kotlin son identificadores válidos
en Java, como por ejemplo 'in'
, 'object'
,
'is'
, etc… Si una biblioteca de Java usa una palabra clave
de Kotlin para un método, se puede escapar usando las comillas
invertidas (`):
// Java
public class Date {
public void when(str:String) { .... }
}
// Kotlin
.`when`("2016") date
Cualquier referencia en Java puede ser nula, lo que hace que los
requisitos de Kotlin de seguridad con los valores nulos no sean
prácticos para los objetos procedentes de Java. Los tipos de
declaraciones de Java se tratan especialmente en Kotlin y se llaman
'platform types'
. Los controles nulos son relajados para
tales tipos, por lo que las garantías de seguridad para ellos son las
mismas que en Java.
val list = ArrayList<String>() // non-null (constructor result)
.add("Item")
listval size = list.size // non-null (primitive int)
val item = list[0] // platform type inferred (ordinary Java object)
.substring(1) // allowed, may throw an exception if item == null item
Kotlin no tiene ‘checked exceptions’. Por lo tanto, los métodos Java que tienen ‘checked exceptions’ se tratan de la misma manera que el resto de métodos.
Al igual que Java se puede usar sin problemas en Kotlin, Kotlin se puede usar fácilmente desde Java.
La JVM no admite funciones de nivel superior. Por lo tanto, para hacer que funcionen con Java, el compilador Kotlin crea una clase Java con el nombre del paquete. Las funciones se definen luego como métodos estáticos Java en esta clase, que deben ser instanciados antes de su uso.
// Kotlin
package org.example.utils
fun cube(n: Int): Int = n * n * n
// Java
import org.example.utils.Utils;
.cube(3); UtilsKt
Como se indica en la sección de “Anotaciones”, podemos indicar al
compilador el nombre del fichero con la anotación
'@JvmName'
:
// Kotlin
@file:JvmName("Utils")
package org.example.utils
fun cube(n: Int): Int = n * n * n
// Java
import org.example.utils.Utils;
.cube(3); Utils
la JVM no tiene soporte para los parámetros por defecto. Por lo
tanto, cuando una función se define con los valores predeterminados, el
compilador debe crear una sola función sin los parámetros
predeterminados. Sin embargo, podemos indicarle al compilador que cree
múltiples sobrecargas de la función para cada parámetro predeterminado
con la anotación '@JvmOverloads'
. Luego, los usuarios de
Java pueden ver las diversas funciones y elegir cuál es la más adecuada.
Esta anotación funciona tanto para constructores, funciones o métodos
estáticos:
// Kotlin
class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) {
@JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { ... }
}
// Java
// Constructors:
(int x, double y)
Foo(int x)
Foo
// Methods
(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { } void f
Los ‘named objects’ y los ‘companion objects’ se
generan como instancias ‘singleton’ de una clase. Sin
embargo, podemos indicar al compilador que genere la función como una
método estático en Java con la anotación '@JvmStatic'
:
// Kotlin
object Console {
fun clear() : Unit { } // Normal
@JvmStatic fun exit() : Unit { } // Con anotación
}
// Java
.INSTANCE.clear() // Normal
Console.exit() // Con anotación Console
En Java, solo podemos detectar las ‘checked exceptions’ si
están declaradas en el método, incluso si el cuerpo del método lanza esa
excepción. Por lo tanto, si tenemos una función que se utilizará desde
Java y queremos permitir que las personas detecten una excepción,
debemos informar al compilador para que agregue la excepción a la firma
del método. Para ello usamos la anotación '@Throws'
:
// Kotlin
@Throws(IOException::class)
fun createDirectory(file: File) {
if (file.exists()) throw IOException("Directory already exists")
.createNewFile()
file}
// Java
try {
.createDirectory(new File("file.txt"));
UtilsKt} catch (IOException e) {
// handle exception here
}
Package specification should be at the top of the source file:
package my.demo
import kotlin.text.*
// ...
An entry point of a Kotlin application is the main()
function:
fun main(args: Array<String>) {
("Hello, World")
println}
Just like most modern languages, Kotlin supports single-line (or end-of-line) and multi-line (block) comments:
// This is an end-of-line comment
/* This is a block comment
on multiple lines. */
Read-only local variables are defined using the keyword
val
. They can be assigned a value only once:
val a: Int = 1 // immediate assignment
val b = 2 // `Int` type is inferred
val c: Int // Type required when no initializer is provided
= 3 // deferred assignment c
Variables that can be reassigned use the var
keyword:
var x = 5 // `Int` type is inferred
+= 1 x
Top-level variables:
val PI = 3.14
var x = 0
fun incrementX() {
+= 1
x }
A reference must be explicitly marked as nullable when
null
value is possible.
var name: String? = null
val length: Int
= name?.length ?: 0 // length, or 0 if name is null
length = name?.length ?: return // length, or return when name is null
length = name?.length ?: throw Error() // length, or throw error when name is null length
Return null
if str
does not hold an
integer:
fun parseInt(str: String): Int? {
// ...
}
Use a function returning nullable value:
fun printProduct(arg1: String, arg2: String) {
val x = parseInt(arg1)
val y = parseInt(arg2)
// Using `x * y` yields error because they may hold nulls.
if (x != null && y != null) {
// x and y are automatically cast to non-nullable after null check
(x * y)
println}
else {
("'$arg1' or '$arg2' is not a number")
println}
}
var a = 1
// simple name in template:
val s1 = "a is $a"
= 2
a // arbitrary expression in template:
val s2 = "${s1.replace("is", "was")}, but now is $a"
In Kotlin, if
can also be used as an expression:
fun bigger(a: Int, b: Int) = if (a > b) a else b
val items = listOf("apple", "banana", "kiwifruit")
for (item in items) {
(item)
println}
val items = listOf("apple", "banana", "kiwifruit")
for (index in items.indices) {
println("item at $index is ${items[index]}")
}
val items = listOf("apple", "banana", "kiwifruit")
var index = 0
while (index < items.size) {
("item at $index is ${items[index]}")
println++
index}
fun numberTypeName(x: Number) = when(x) {
0 -> "Zero" // Equality check
in 1..4 -> "Four or less" // Range check
5, 6, 7 -> "Five to seven" // Multiple values
is Byte -> "Byte" // Type check
else -> "Some number"
}
fun describe(obj: Any): String =
when (obj) {
1 -> "One"
"Hello" -> "Greeting"
is Long -> "Long"
!is String -> "Not a string"
else -> "Unknown"
}
fun signAsString(x: Int)= when {
< 0 -> "Negative"
x == 0 -> "Zero"
x else -> "Positive"
}
Function having two Int
parameters with Int
return type:
fun sum(a: Int, b: Int): Int {
return a + b
}
Function with an expression body and inferred return type:
fun sum(a: Int, b: Int) = a + b
Function returning no meaningful value:
fun printSum(a: Int, b: Int): Unit {
("sum of $a and $b is ${a + b}")
println}
Unit
return type can be omitted:
fun printSum(a: Int, b: Int) {
("sum of $a and $b is ${a + b}")
println}
() -> Unit
- takes no arguments and returns nothing
(Unit). (Int, Int) -> Int
- takes two arguments of type
Int and returns Int. (() -> Unit) -> Int
- takes
another function and returns Int. (Int) -> () -> Unit
- takes argument of type Int and returns function.
// Simple lambda expression
val add: (Int, Int) -> Int = { i, j -> i + j }
val printAndDouble: (Int) -> Int = {
(it)
println// When single parameter, we can reference it using `it`
* 2 // In lambda, last expression is returned
it }
// Anonymous function alternative
val printAndDoubleFun: (Int) -> Int = fun(i: Int): Int {
(i) // Single argument can’t be referenced by `it`
printlnreturn i * 2 // Needs return like any function
}
val i = printAndDouble(10) // 10
(i) // 20 print
fun Int.isEven() = this % 2 == 0
(2.isEven()) // true
print
fun List<Int>.average() = 1.0 * sum() / size
(listOf(1, 2, 3, 4).average()) // 2.5 print
// val declares a read-only property, var a mutable one
class Person(val name: String, var age: Int)
// name is read-only, age is mutable
open class Person(val name: String) {
open fun hello() = "Hello, I am $name"
// Final by default so we need open
}
class PolishPerson(name: String) : Person(name) {
override fun hello() = "Dzień dobry, jestem $name"
}
class Person(var name: String, var surname: String) {
var fullName: String
get() = "$name $surname"
set(value) {
val (first, rest) = value.split(" ", limit = 2)
= first
name = rest
surname }
}
data class Person(val name: String, var age: Int)
val mike = Person("Mike", 23)
// Modifier data adds:
// 1. toString that displays all primary constructor properties
(mike.toString()) // Person(name=Mike, age=23)
print
// 2. equals that compares all primary constructor properties
(mike == Person("Mike", 23)) // True
print(mike == Person("Mike", 21)) // False
print
// 3. hashCode that is based on all primary constructor properties
val hash = mike.hashCode()
(hash == Person("Mike", 23).hashCode()) // True
print(hash == Person("Mike", 21).hashCode()) // False
print
// 4. component1, component2 etc. that allows deconstruction
val (name, age) = mike
("$name $age") // Mike 23
print
// 5. copy that returns copy of object with concrete properties changed
val jake = mike.copy(name = "Jake")
(1,2,3,4) // List<Int>
listOf(1,2,3,4) // MutableList<Int>
mutableListOf
("A", "B", "C") // Set<String>
setOf("A", "B", "C") // MutableSet<String>
mutableSetOf
('a', 'b', 'c') // Array<Char>
arrayOf
(1 to "A", 2 to "B") // Map<Int, String>
mapOf(1 to "A", 2 to "B")
mutableMapOf// MutableMap<Int, String>
(4,3,2,1) // Sequence<Int>
sequenceOf
1 to "A" // Pair<Int, String>
(4) { it * 2 } // List<Int>
List(4) { it + 2 } // Sequence<Int> generateSequence
students.fiter { it.passing && it.averageGrade > 4.0 }
// Only passing students
.sortedByDescending { it.averageGrade }
// Starting from ones with biggest grades
.take(10) // Take first 10
.sortedWith(compareBy({ it.surname }, { it.name }))
// Sort by surname and then name
(0) { it + 1 }
generateSequence// Infinitive sequence of next numbers starting on 0
.filter { it % 2 == 0 } // Keep only even
.map { it * 3 } // Triple every one
.take(100) // Take first 100
.average() // Count average
// Most important functions for collection processing
val l = listOf(1,2,3,4)
//filter - returns only elements matched by predicate
.filter { it % 2 == 0 } // [2, 4]
l
// map - returns elements after transformation
.map { it * 2 } // [2, 4, 6, 8]
l
// flatMap - returns elements yielded from results of trans.
.flatMap { listOf(it, it + 10) } // [1, 11, 2, 12, 3, 13, 4, 14]
l
// fold/reduce - accumulates elements
.fold(0.0) { acc, i -> acc + i } // 10.0
l.reduce { acc, i -> acc * i } // 24
l
// forEach/onEach - perfons an action on every element
.forEach { print(it) } // Prints 1234, returns Unit
l.onEach { print(it) } // Prints 1234, returns [1, 2, 3, 4]
l
// partition - splits into pair of lists
val (even, odd) = l.partition { it % 2 == 0 }
(even) // [2, 4]
print(odd) // [1, 3]
print
// min/max/minBy/maxBy
.min() // 1, possible because we can compare Int
l.minBy { -it } // 4
l.max() // 4, possible because we can compare Int
l.maxBy { -it } // 1
l
// first/firstBy
.first() // 1
l.first { it % 2 == 0 } // 2 (first even number)
l
// count - count elements matched by predicate
.count { it % 2 == 0 } // 2
l
// sorted/sortedBy - returns sorted collection
(2,3,1,4).sorted() // [1, 2, 3, 4]
listOf.sortedBy { it % 2 } // [2, 4, 1, 3]
l
// groupBy - group elements on collection by key
.groupBy { it % 2 } // Map: {1=[1, 3], 0=[2, 4]}
l
// distinct/distinctBy - returns only unique elements
(1,1,2,2).distinct() // [1, 2] listOf
val list = mutableListOf(3,4,2,1)
val sortedResult = list.sorted() // Returns sorted
(sortedResult) // [1, 2, 3, 4]
println(list) // [3, 4, 2, 1]
println
val sortResult = list.sort() // Sorts mutable collection
(sortResult) // kotlin.Unit
println(list) // [1, 2, 3, 4] println
Returns ‘Receiver’ | Returns ‘Results of lambda’ | |
---|---|---|
Reference to receiver: ‘it’ | also | let |
Reference to receiver: ‘this’ | apply | run/with |
val dialog = Dialog().apply {
= "Dialog title"
title { print("Clicked") }
onClick }
// Lazy - calculates value before first usage
val i by lazy { print("init "); 10 }
(i) // Prints: init 10
print(i) // Prints: 10
print
// notNull - returns last setted value, or throws error if no value has been set
// observable/vetoable - calls function every time value changes. In vetoable function also decides if new value should be set.
var name by observable("Unset") { p, old, new ->
("${p.name} changed $old -> $new")
println}
= "Marcin"
name // Prints: name changed Unset -> Marcin
// Map/MutableMap - finds value on map by property name
val map = mapOf("a" to 10)
val a by map
(a) // Prints: 10 print
Modifier | Class members | Top-level |
---|---|---|
Public (default) | Visible everywhere | Visible everywhere |
Private | Visible only in the same class | Visible in the same class |
Protected | Visible only in the sambe class and subclasses | Not allowed |
Internal | Visible in the same module if class is accessible | Visible in the same module |
Esta obra está bajo una licencia de
Creative Commons Reconocimiento-Compartir Igual 4.0
Internacional.