Leoš Přikryl
@leos_prikryl

commity.cz, GDG Jihlava

Kotlin

  • Jazyk nad JVM od JetBrains
  • Staticky typovaný
  • 100% interoperabilní s Javou

Historie

“We’ve built tools to support so many nice languages and we’re still using Java”

JetBrains, 2010

Historie

  • 2010 - první commit
  • únor 2016 - verze 1.0
  • aktuálně - verze 1.2.21

Proč potřebujeme další jazyk nad JVM?

  • Vývoj Javy je pomalý
  • Zaostává za moderními jazyky
  • Boilerplate code
  • ... ale má skvělý ekosystém

Proč Kotlin

  • Snadno pochopitelný pro Java vývojáře
  • Navržen vývojáři
  • Stručný a pragmatický
  • Kombinace funkcionálního a objektového přístupu
  • Zpětná kompatibilita
  • Stejně rychlý jako Java
  • Skvělá podpora v IntelliJ IDEA

Kotlin je brán vážně

  • Od léta 2017 oficiální jazyk pro Android
  • Podpora Kotlinu ve Springu 5
  • Gradle build scripts v Kotlinu
  • AWS, Pinterest, Basecamp, Coursera, Netflix, Uber, Square, Trello, ...

Další alternativy

Základy syntaxe

Deklarace proměnné

val a: Int = 2              //immutable
var b: String = "JavaDays"  //mutable
Type inference
val a = 2
var b = "JavaDays"
val myClass = MyClass()
Další postřehy - chybí klíčové slovo "new", středníky jsou nepovinné

Imutabilní kolekce

val list = listOf(1, 2, 3)          //immutable
val list = mutableListOf(1, 2, 3)   //mutable

val map = mapOf(1 to "one", 2 to "two")         //immutable
val map = mutableMapOf(1 to "one", 2 to "two")  //mutable

If conditions

var max: Int
if (a > b) {
    max = a
} else {
    max = b
}
If je výraz
val max = if (a > b) a else b
Kotlin nemá ternární operátor ? :

When

var text: String
when (x) {
    1 -> text = "jedna"
    2 -> text = "dvě"
    else -> {
        text = "mnoho"
    }
}
When je výraz
val text = when (x) {
    1 -> "jedna"
    2 -> "dvě"
    else -> {
        "mnoho"
    }
}

When podruhé

val text = when (x) {
    1, 2 -> "jedna nebo dvě"
    3..10 -> "tři až deset"
    else -> "mnoho"
}

When potřetí

when {
    x.isOdd() -> println("x je liché")
    x.isEven() -> println("x je sudé")
    else -> throw IllegalNumberException()
}
if (x.isOdd()) {
    System.out.println("x je liché");
} else if (x.isEven()) {
    System.out.println("x je sudé");
} else {
    throw new IllegalNumberException();
}

Funkce

fun sum(a: Int, b: Int = 0): Int {
    return a + b
}
fun sum(a: Int, b: Int = 0) = a + b
Defaultní parametry mohou nahradit přetěžování funkcí
public int sum(int a, int b) {
    return a + b;
}

public int sum(int a) {
    return sum(a, 0) ;
}

Higher-order functions

public inline fun measureTimeMillis(block: () -> Unit) : Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}
val elapsedTime = measureTimeMillis {
    doSomeWork()
}
Lambda funkce
val doubled = ints.map { value -> value * 2 }
val doubled = ints.map { it * 2 }

Vlastní řídící struktury

fun unless(conditional: Boolean, body: () -> Unit) {
    if (!conditional) {
        body()
    }
}

unless(age >= 18) {
    //no beer :-(
}

Vlastní DSL

val tree = createHTMLDocument().html {
    body {
        h1 {
            +"title"
        }
        div {
            +"content"
        }
    }
}

Problémy, které Kotlin řeší

Properties, primary constructor

public class User {

    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
class User (var name: String)

Data classes

public class User {

    private String name;

    public User(String name) {...}

    public String getName() {...}

    public void setName(String name) {...}

    @Override
    public boolean equals(Object o) {...}

    @Override
    public int hashCode() {...}

    @Override
    public String toString() {...}

    public User copy(String name) {...}
}
data class User (var name: String)

Data classes - použití

data class User(val name: String = "", val email: String = "")

val user = User(name = "Petr")
val user2 = user.copy(email = "petr@gmail.com")

Intuitivní equals

val person1 = Person("Petr")
val person2 = Person("Petr")
person1 == person2   //true - automaticky volá .equals()
person1 === person2  //false - porovnává reference

String interpolation

String s = "Rozměry: " + width + " x " + height + " metrů";String s = String.format("Rozměry: %d x %d metrů", width, height);
val s = "Rozměry: $width x $height metrů"

Multiline strings

String sql =
    "SELECT\n" +
    "    fist_name,\n" +
    "    last_name,\n" +
    "    email\n" +
    "FROM users\n" +
    "WHERE id = ?";
val sql =
    """SELECT
            fist_name,
            last_name,
            email
       FROM users
       WHERE id = ?"""

Smart casts

if (obj instanceof String) {
    System.out.println(((String) obj).toLowerCase());
}
if (obj is String) {
    println(obj.toLowerCase());
}

Smart casts - when

if (value instanceof Number) {
    cell.setDoubleValue(((Number)value).toDouble());
} else if (value instanceof Date) {
    cell.setDateValue((Date)value);
} else {
    cell.setStringValue(value.toString());
}
when (value) {
    is Number -> cell.setDoubleValue(value.toDouble())
    is Date -> cell.setDateValue(value)
    else -> cell.setStringValue(value.toString())
}

No checked exceptions

try {
    URLEncoder.encode(url, "UTF-8");
} catch (UnsupportedEncodingException ignore) {
    //should never happen
}
URLEncoder.encode(url, "UTF-8")

Singleton

public class Singleton {

    private static Singleton instance = null;

    private Singleton(){
    }

    private synchronized static void createInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
    }

    public static Singleton getInstance() {
        if (instance == null) createInstance();
        return instance;
    }
}
object Singleton

Lazy initialization

private ExpensiveObject expensiveObject;

public ExpensiveObject getExpensiveObject() {
    if(expensiveObject == null) {
        synchronized(this) {
            if(expensiveObject == null) {
                expensiveObject = new ExpensiveObject();
            }
        }
    }

    return expensiveObject;
}
val expensiveObject by lazy  { ExpensiveObject() }

Observable property

class User {
    var name: String by observable("N/A") { property, oldValue, newValue ->
        println("Changing ${property.name} from $oldValue to $newValue")
}

Vetoable property

class Address {
    var zip: String by vetoable("58601") { _, _, newValue ->
        newValue.length == 5
    }
}

Null safety

var notNullString: String = "JavaDays"
notNullString = null  //compilation error
var nullableString: String? = "JavaDays"
notNullString = null  //OK

Null safe operator

fun getZipForUser(user: User?): String? {
    return user?.address?.zip
}
public String getZipForUser(User user) {
    if (user != null && user.getAddress() != null) {
        return user.getAddress().getZip();
    } else {
        return null;
    }
}

Safe calls

val locationService = getLocationService()
val location = locationService?.findLocation()
if (location != null) {
    println("${location.longitude}, ${location.latitude}")
}
LocationService locationService = getLocationService();
if (locationService != null) {
    Location location = locationService.findLocation();
    if (location != null) {
        println(location.longitude + ", " + location.latitude);
    }
}

Elvis operator

val displayName = username ?: "N/A"
String displayName = username != null ? username : "N/A";

Elvis operator

fun getUserById(id: Long) : User {
    return userRepository.findOne(id) ?: throw UserNotFoundException()
}
User getUserById(long id) {
    User user = userRepository.findOne(id);
    if (user == null) {
        throw new UserNotFoundException();
    }
    return user;
}

Rozšíření API

Extension funkce

public inline fun String.isEmpty(): Boolean = length == 0
Nahrazují *Utils (StringUtils, FileUtils, ...) třídy
if (StringUtils.isEmpty(string)) {
if (string.isEmpty()) {

Extension funkce ve standardní knihovně

  • String - substringBefore, isNullOrEmpty, removePrefix, ...
  • Collections - filter, map, sum, min, max, groupBy, ...
  • I/O - String.byteInputStream(), InputStream.readBytes()

Ukázka Collections extensions 1

List<String> oldList =
    Arrays.asList("a1", "a2", "b1", "c2", "c1");

List<String> transformedList = oldList
    .stream()
    .filter(s -> s.startsWith("c"))
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList());

// C1, C2
val oldList = listOf("a1", "a2", "b1", "c2", "c1")

val transformedList = oldList
    .filter { it.startsWith("c") }
    .map { it.toUpperCase() }
    .sorted()

// C1, C2

Ukázka Collections extensions 2

Map<Integer, List<String>> groupsByLength = new HashMap<>();
for (String s : collection) {
    List<String> strings = groupsByLength.get(s.length());
    if (strings == null) {
        strings = new ArrayList<>();
        groupsByLength.put(s.length(), strings);
    }
    strings.add(s);
}
val groupsByLength = collection.groupBy{ it.length }

Spring

Constructor injection

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    //...
}
@Service
class UserService(
    private val userRepository: UserRepository
) {
    //...
}

DTO / POJO

public class User {

    private String name;

    public User(String name) {...}

    public String getName() {...}

    public void setName(String name) {...}

    @Override
    public boolean equals(Object o) {...}

    @Override
    public int hashCode() {...}

    @Override
    public String toString() {...}

    public User copy(String name) {...}
}
data class User (var name: String)

Extensions (Spring 5)

ParameterizedTypeReference<List<User>> typeReference = new ParameterizedTypeReference<List<User>>(){};
ResponseEntity<List<User>> responseEntity = restTemplate.exchange("/users", HttpMethod.GET, null, typeReference);
List<User> users = responseEntity.getBody();
val users: List<User>? = restTemplate.getForObject("/users")

Využití @NonNull a @Nullable pro null safety (Spring 5)

  • Anotace jsou doplněny do většiny tříd ve Springu
  • Kotlin pozná, jestli je parametr nebo návratová hodnota nullable
  • Defaultně pouze warning, lze změnit i na error

Využití nullable typů pro required (Spring 5)

@GetMapping("orders")
List<Order> getOrders(@RequestParam(required = false) Date fromDate,
                      @RequestParam(required = false) Date toDate) {
    return orderService.getOrders(fromDate, toDate);
}
@GetMapping("orders")
fun getOrders(@RequestParam fromDate: Date?,
              @RequestParam toDate: Date?) : List<Order> {
    return orderService.getOrders(fromDate, toDate)
}

Problém 1

Třídy v Kotlinu jsou defaultně final
@Service
open class UserService(
    private val userRepository: UserRepository
) {
    //...
}
Řešení: 
kotlin-spring Gradle/Maven plugin

Problém 2

Chybí no-arg konstruktor pro JPA Entity
data class User (var name: String = "")
Řešení: 
kotlin-jpa Gradle/Maven plugin

Problém 3

Klíčová slova Kotlinu v testovacích frameworcích
Mockito.`when`(userService.getAllUsers())
    .thenReturn(listOf(User("Petr"))
Řešení: 
https://github.com/nhaarman/mockito-kotlin
Mockito.whenever(userService.getAllUsers())
    .thenReturn(listOf(User("Petr"))

Problém 4

Anotace na data class
@Entity
data class User (
    @Id
    var id: Int,

    @Column(unique = true)
    var email: String
)
Řešení:
@Entity
data class User (
    @field:Id
    var id: Int,

    @field:Column(unique = true)
    var email: String
)

Ekosystém

Knihovny / frameworky

  • 100% interoperabilita s Javou
  • Kotlin Standard Library - extensions
  • Kotlin-specific knihovny
    • Ktor
    • Html
    • KLogging
    • FunKTionale

Interoperabilita s Javou

public class User {

    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
val user = User("John")
println(user.name)

val file = File("temp.tmp")
if (file.exists()) {
    println("File exists")
}
val runnable = Runnable { println("runnable") }

Tooling

  • Build - Gradle, Maven
  • IDE - IntelliJ IDEA
    • Převod z Javy do Kotlinu (i copy & paste)
    • Napovídání idiomatických konstrukcí

Budoucnost

Full-stack aplikace

  • Backend
  • Android
  • Browser
  • iOS

Kotlin everywhere!

Jak začít

Hned ;-)

Nový projekt

Stávající projekt

  • Lze bez problémů míchat Javu a Kotlin
    • Konvertor Javy do Kotlinu v IntelliJ IDEA

 

  • Případně rovnou konverze celého projektu
    • Spring aplikace, cca 400 souborů ~ 4 hodiny
    • 18 476 řádků redukováno na 11 530

Ukázka

Odkazy

Dotazy