Введение
В преддверии выхода языка Kotlin из beta, хочется поделиться своим впечатлением от его использования.
Kotlin — это новый прекрасный язык от JetBrains (разработчиков IntelliJ Idea) для JVM, Android и браузера, который на первый взгляд выглядит как улучшенная java (или как упрощенная scala). Но это только на первый взгляд, язык не только впитал в себя многие интересные решения от других языков, но и представляет свои оригинальные:
— optional от swift, nullsafe в kotlin
— case классы из scala, data class в kotlin
— замена implicit методам из scala, extension функций
— делегаты
— null safely
— smart cast
— и многое другое, подробнее можно посмотреть на официальном сайте kotlinlang.
Для тех кто знаком с java или scala, будет интересно сравнение kotlin & java, kotlin & scala.
Авторы языка стремятся добиться двух задач:
— сделать скорость компиляции сравнимой с java
— язык должен быть достаточно выразительным, и при этом быть простым насколько возможно
Поэтому, стоит оговориться, что если вы на текущей момент счастливы со scala, с ее «сложностью» и временем компиляции, тогда вам скорее всего не нужен будет kotlin, для всех остальных читать дальше:
Для тех кто
Hello world
package hello
fun main(args: Array<String>) {
println("Hello World!")
}
Чтение аргументов
fun main(args: Array<String>) {
if (args.size() == 0) {
println("Provide a name")
return
}
println("Hello, ${args[0]}!")
}
hello world c ООП
class Greeter(val name: String) {
fun greet() {
println("Hello, $name")
}
}
fun main(args: Array<String>) {
Greeter(args[0]).greet()
}
Из личного опыта применения kotlin особо хочется отметить несколько преимуществ языка:
— первое это конечно простоту взаимодействия с java. Все типы и коллекции из java преобразовываются в аналогичные из kotlin, и наоборот. Это особенно радует после всей той «анархии», которая творится в scala (да есть scala.collection.JavaConversions._ и scala.collection.JavaConverters._, но все же это не сравниться с полностью прозрачной конвертацией типов);
— также не может не радовать отличная поддержка от студии Intellij Idea, хоть язык и находится в Beta 4, уже на текущий момент плагин для студии позволяет комфортно работать;
— а для любителей implicit методов из scala, kotlin преподносит очень удобное решение в виде extension функций;
— помимо всего прочего разработчики языка ставят своей целью добиться времени компиляции сравнимой с java (привет scala), за что им только хочется пожать руки! Это особенно сильно радует после долгой работы в scala, когда редактирование одной строчки в достаточно небольшом файле компилируется с той же скоростью что и небольшой проект на java;
— inline функции — отличное нововведение. С их помощью можно, например, расширить текущие возможности языка, или в некоторых ситуациях добиться повышения производительности;
— удобные функции стандартной библиотеки.
— удобные лямбды, в отличие от той же java 8. Очень похожи на реализацию из scala.
Тем не менее у языка есть и свои недостатки:
— не хватает pattern matching из scala, но в некоторых ситуациях спасает smart cast и Destructuring Declarations, в других же приходится выкручиваться другими средствами. Отсутствие pattern matching в целом понятно, разработчики стараются добиться максимального приближения к времени компиляции java, но его наличие позволило бы существенно упростить написание некоторых приложений, так что довольствуемся тем что есть;
— try with resource пока реализован не очень удачно. Но тут авторы языка обещают в ближайшее время исправить ситуацию. А пока можно либо применять имеющееся решение, либо воспользоваться расширением языка:
try-with-resources
internal class ResourceHolder : AutoCloseable {
val resources = ArrayList<AutoCloseable>()
fun <T : AutoCloseable> T.autoClose(): T {
resources.add(this)
return this
}
override fun close() {
resources.reverse()
resources.forEach {
try {
it.close()
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
}
inline internal fun <R> using(block: ResourceHolder.() -> R): R {
val holder = ResourceHolder()
try {
return holder.block()
} finally {
holder.close()
}
}
Пример использования
fun copy(from: Path, to: Path) {
using {
val input = Files.newInputStream(from).autoClose()
val output = Files.newOutputStream(to).autoClose()
input.copyTo(output)
}
}
— пока нет async и yield, но по словам авторов, после релиза 1.0 можно ждать их появление в самом ближайшем будущем.
Update В комментариях наибольший интерес вызывает сравнение со scala, и в частности преимущество kotlin:
Преимущества kotlin по сравнению со scala
— взаимодействие с java и обратно. На kotlin вообще можно забыть про то что ты вызываешь что-то из java, все предельно прозрачно и удобно. Тоже самое наоборот. Из java также удобно работать с kotlin. Тут даже не только сходство типов помогает, сколько сама ориентированность kotlin на прозрачную работу с java. Проще всего это продемонстрировать на примерах:
— отсутствие implicit. Сами по себе implicit конечно мощное решение, но без студии разобраться в чужом коде что к чем, почти нереально. Особенно если авторы «сильно увлеклись». А для добавления методов kotlin позволяет писать extension, которые на практике выглядят гораздо удобнее аналогичных решений в scala. Пример:
Scala:
Kotlin:
— nullable типы вместо Option.
— удобные функции стандартной библиотеки
— ну и конечно же скорость компиляции. На практике действительно все очень быстро компилируется, особенно по сравнению со scala.
— результирующий jar меньше аналогичного в scala.
— и что немаловажно, JetBrains обещают полную обратную совместимость (привет еще раз scala c либами под 2.10, 2.11, 2.12 и т.д.)
Примеры
1. Использование лямбд вместо анонимных классов (в scala правда тоже скоро это может появиться, но пока такой возможности нет).
Код в java:
В scala же уже не получится такое провернуть. А вот с kotlin проблем нет:
2. Есть тот же try-with-resources (да, можно сделать extension к языку, но из коробки этого нет, а значит придется тянуть решение из одного проекта в другой)
Это по крайней мере что первое приходит в голову
Код в java:
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.execute(System.out::println);
В scala же уже не получится такое провернуть. А вот с kotlin проблем нет:
val executor = Executors.newFixedThreadPool(1)
executor.execute { println() }
2. Есть тот же try-with-resources (да, можно сделать extension к языку, но из коробки этого нет, а значит придется тянуть решение из одного проекта в другой)
Это по крайней мере что первое приходит в голову
— отсутствие implicit. Сами по себе implicit конечно мощное решение, но без студии разобраться в чужом коде что к чем, почти нереально. Особенно если авторы «сильно увлеклись». А для добавления методов kotlin позволяет писать extension, которые на практике выглядят гораздо удобнее аналогичных решений в scala. Пример:
Scala:
implicit class RichInt(val x: Int) extends AnyVal {
def square: Int = x * x
}
object App {
def print(): Unit = {
val two = 2
println(s"The square of 2 is ${two.square}")
}
}
Kotlin:
fun Int.richInt(): Int = this * this
object App {
fun print(): Unit {
val two = 2
println("The square of 2 is ${two.richInt()}")
}
}
— nullable типы вместо Option.
— удобные функции стандартной библиотеки
— ну и конечно же скорость компиляции. На практике действительно все очень быстро компилируется, особенно по сравнению со scala.
— результирующий jar меньше аналогичного в scala.
— и что немаловажно, JetBrains обещают полную обратную совместимость (привет еще раз scala c либами под 2.10, 2.11, 2.12 и т.д.)
Перейдем к примеру, в котором будет продемонстрировано небольшое RESTful приложение на spring boot, со сборкой через gradle.
Настройка студии
Для работы необходимо поставить IntelliJ Idea Community (но можно использовать и Eclipse, под нее также есть плагин), в которой после установки обновить плагин kotlin. Обновить его необходимо вручную, через settings -> plugin, даже если вы перед этим выбрали обновление плагина через всплывающее окно (по крайней мере на данный момент, пока язык в beta).
Также лучше поставить локальный gradle, и прописать его в настройках в студии (settings -> build, execution, deployment -> gradle -> user local gradle distribution. После чего указать путь к gradle в gradle home).
Настройка проекта
Создаем проект gradle kotlin (new project -> gradle -> kotlin) и изменяем содержимое build.gradle на следующее:
Содержимое build.gradle
buildscript {
ext.kotlin_version = '1.0.0-beta-4584'
repositories {
mavenCentral()
maven { url "http://repo.spring.io/snapshot" }
maven { url "http://repo.spring.io/milestone" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.0.RELEASE")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'kotlin'
jar {
baseName = 'test-spring-kotlin-project'
version = '0.1.0'
}
repositories {
mavenCentral()
maven { url "http://repo.spring.io/snapshot" }
maven { url "http://repo.spring.io/milestone" }
maven { url "http://10.10.10.67:8081/nexus/content/groups/public" }
}
dependencies {
compile("org.springframework.boot:spring-boot-starter-web:1.3.0.RELEASE")
compile("org.springframework:spring-jdbc:4.2.3.RELEASE")
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.6.4")
compile("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
}
Создаем файл application.properties в папке src/main/resources, в котором укажем порт для запуска spring boot:
application.properties
server.port = 8080
Создаем файл Application.kt в папке src/main/kotlin/test.kotlin.spring.project. В нем будут основные настройки для запуска spring boot:
Application.kt
package test.kotlin.spring.project
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.boot.context.web.SpringBootServletInitializer
import org.springframework.context.annotation.Bean
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
@SpringBootApplication
@EnableAutoConfiguration(exclude = arrayOf(DataSourceAutoConfiguration::class))
open class Application : SpringBootServletInitializer() {
@Bean
open fun mapperForKotlinTypes(): MappingJackson2HttpMessageConverter {
return MappingJackson2HttpMessageConverter().apply { objectMapper = jacksonMapper }
}
override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder =
application.sources(Application::class.java)
companion object {
val jacksonMapper = ObjectMapper().registerKotlinModule()
.setSerializationInclusion(JsonInclude.Include.NON_ABSENT)
.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
@Throws(Exception::class)
@JvmStatic fun main(args: Array<String>) {
println("starting application...")
SpringApplication.run(Application::class.java, *args)
}
}
}
Также необходимо будет создать файл с настройками методов rest сервиса. Будет несколько методов:
— метод будет выдавать AckResponse на введенные с запроса данные об имени и фамилии.
— метод, на вход поступает массив строк, из которого выбирается наименьшая строка по длине, которая потом разбивается по '_', сортируется и собирается в строку уже с символом ',' (демонстрирует возможности языка)
Создаем файл ServiceController.kt в папке src/main/kotlin/test.kotlin.spring.project.
ServiceController.kt
package test.kotlin.spring.project
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
data class AckResponse(val status: Boolean, val result: String, val message: String? = null)
@RestController
class ServiceController {
@RequestMapping(
path = arrayOf("/request"),
method = arrayOf(RequestMethod.GET),
produces = arrayOf(MediaType.APPLICATION_JSON_UTF8_VALUE))
fun nameRequest(
@RequestParam(value = "name") name: String,
@RequestParam(value = "surname", required = false) surname: String?): AckResponse {
return if (surname == null)
AckResponse(status = true, result = "Hi $name", message = "surname is empty")
else
AckResponse(status = true, result = "Hi $surname,$name")
}
@RequestMapping(
path = arrayOf("/sort_request"),
method = arrayOf(RequestMethod.GET),
produces = arrayOf(MediaType.APPLICATION_JSON_UTF8_VALUE))
fun findMinimum(
@RequestParam(value = "values") values: Array<String>): AckResponse {
println("values:")
values.forEach { println(it) }
val minValue = values.apply { sortBy { it.length } }
.firstOrNull()
?.split("_")
?.sorted()
?.joinToString(",") ?: ""
return AckResponse(status = true, result = minValue)
}
}
Запуск и проверка работы
Запускаем приложение из Application.kt. В случае успешного запуска в логе будет что-то вроде:
Логи приложения
starting application... . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.3.0.RELEASE) 2016-01-12 12:47:48.242 INFO 88 --- [ main] t.k.s.project.Application$Companion : Starting Application.Companion on Lenovo-PC with PID 88 (D:\IDA_Projects\test\build\classes\main started by admin in D:\IDA_Projects\test) 2016-01-12 12:47:48.247 INFO 88 --- [ main] t.k.s.project.Application$Companion : No profiles are active 2016-01-12 12:47:48.413 INFO 88 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@dbf57b3: startup date [Tue Jan 12 12:47:48 MSK 2016]; root of context hierarchy 2016-01-12 12:47:50.522 INFO 88 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'beanNameViewResolver' with a different definition: replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]] 2016-01-12 12:47:51.066 INFO 88 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$ede1977c] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2016-01-12 12:47:51.902 INFO 88 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http) 2016-01-12 12:47:51.930 INFO 88 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat 2016-01-12 12:47:51.937 INFO 88 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.0.28 2016-01-12 12:47:52.095 INFO 88 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2016-01-12 12:47:52.095 INFO 88 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 3688 ms 2016-01-12 12:47:52.546 INFO 88 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/] 2016-01-12 12:47:52.556 INFO 88 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*] 2016-01-12 12:47:52.557 INFO 88 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] 2016-01-12 12:47:52.559 INFO 88 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*] 2016-01-12 12:47:52.559 INFO 88 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*] 2016-01-12 12:47:52.985 INFO 88 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@dbf57b3: startup date [Tue Jan 12 12:47:48 MSK 2016]; root of context hierarchy 2016-01-12 12:47:53.089 INFO 88 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/request],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public final test.kotlin.spring.project.AckResponse test.kotlin.spring.project.ServiceController.pullUpdate(java.lang.String,java.lang.String) 2016-01-12 12:47:53.094 INFO 88 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest) 2016-01-12 12:47:53.094 INFO 88 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest) 2016-01-12 12:47:53.138 INFO 88 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2016-01-12 12:47:53.139 INFO 88 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2016-01-12 12:47:53.195 INFO 88 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2016-01-12 12:47:53.512 INFO 88 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup 2016-01-12 12:47:53.612 INFO 88 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) 2016-01-12 12:47:53.620 INFO 88 --- [ main] t.k.s.project.Application$Companion : Started Application.Companion in 6.076 seconds (JVM running for 7.177) 2016-01-12 12:47:57.874 INFO 88 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' 2016-01-12 12:47:57.874 INFO 88 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 2016-01-12 12:47:57.897 INFO 88 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 23 ms
После успешного запуска, пробуем открыть страницу Запрос с именем. Ответ должен выглядеть следующем образом:
{
status: true,
result: "Hi Kris",
message: "surname is empty"
}
И Запрос с именем и фамилией, тогда ответ будет немного другой:
{
status: true,
result: "Hi Eagle, Kris"
}
Вызов для проверки сортировки данных: Сортировка. В результате должно быть:
{
status: true,
result: "1,3,value,virst"
}
Тот же вызов, но с пустым массивом: Вызов
{
status: true,
result: ""
}
При необходимости можно собрать весь проект в один runnable jar, командой: gradle build. В результате проект будет собран в один архив, содержащий все зависимости без распаковки. При таком подходе существенно повышается время сборки проекта, по сравнению с тем же assemble, когда проект собирается в один архив с распаковкой всех зависимостей.
Заключение
В заключении хочется отметить что kotlin оказался весьма удобным языком для работы над любым проектом, где используется java, в качестве ее замены. Экосистема языка пока не такая обширная как та же scala, но уже сейчас можно использовать его в том же big data, где есть java api. К тому же из kotlin очень просто взаимодействовать с java, так что все что есть в java, можно использовать и в kotlin. К тому же из студии есть возможность легкой конвертации java файлов в аналогичные на kotlin (правда нужно будет немного руками подправить файл после конвертации). JetBrains проделали замечательную работу на пути создания идеального языка на смену java и scala. И надеюсь в будущем тенденция в сторону использования kotlin будет только расти.
Исходники доступны на github.
Спасибо всем за внимание.