写在开头:本人打算开始写一个Kotlin系列的教程,一是使本身记忆和理解的更加深入,二是能够分享给一样想学习Kotlin的同窗。系列文章的知识点会以《Kotlin实战》这本书中顺序编写,在将书中知识点展现出来同时,我也会添加对应的Java代码用于对比学习和更好的理解。java
Kotlin教程(一)基础
Kotlin教程(二)函数
Kotlin教程(三)类、对象和接口
Kotlin教程(四)可空性
Kotlin教程(五)类型
Kotlin教程(六)Lambda编程
Kotlin教程(七)运算符重载及其余约定
Kotlin教程(八)高阶函数
Kotlin教程(九)泛型git
Kotlin代码在编译后都会转成Java文件,对于编写的Kotlin,咱们能够经过工具提早看到转换后的Java代码,具体方式是: 在as中找到Tools>Kotlin>Show Kotlin Bytecode,而后点面板上的Decompile。express
对于以前写好的Java代码,咱们也能够用工具转换成Kotlin代码,方法是: Code > Convert Java File To Kotlin File编程
学习就从如何用Kotlin编写一个“Hollo World”开始吧!先看熟悉的Java:小程序
public static void main(String[] args) {
System.out.println("Hello, world!");
}
复制代码
而后那,是Kotlin的写法:数组
fun main(args: Array<String>) {
println("Hello, world!")
}
复制代码
能够看到Kotlin中:bash
fun
关键字用来声明一个函数;dom
main
是方法名;函数
args: Array<String>
表示参数,能够发现于java中先类型后变量名相反,Kotlin中是先变量名,而后:
,而后是类型声明。工具
Kotlin中没有声明数组的特殊语法,而是用Array表示数组,有点相似集合的感受;
println
代替了System.out.println
,这是Kotlin标准库给Java标准库函数提供了许多语法更简洁的包装;
不知道你有没有注意到;
,Kotlin中省略了分号。
熟悉Java的你可能会想返回值在哪里那?怎么没有那?
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
复制代码
: Int
这里就出现,表示返回值是一个int类型,那为何上面那个函数没有写那?其实上面那个函数也有返回值,返回值是空,也就是void,在Kotlin中实际上是: Unit
,而: Unit
默承认以省略,因此就看不到返回值的声明了。
一样方法在对比Java的看下:
public static int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
复制代码
是否是发现了方法体中if的使用好像有区别? 在Kotlin中,if是表达式,而不是语句。语句和表达式的区别在于,表达式有值,而且能做为另外一个表达式的一部分使用;而语句老是包围着它的代码块中的顶层元素,而且没有本身的值。在Java中,所用的控制结构都是语句。而在Kotlin中,除了循环(for,do,do/while)之外大多数控制结构都是表达式。
若是一个方法的函数体是由单个表达式构成的,能够用这个表达式做为完整的函数体,而且去掉花括号和return语句:
fun max(a: Int, b: Int) = if (a > b) a else b
复制代码
ps:在as中经过alt+enter能够唤起操做,提供了在两种函数风格之间转换的方法:"Convert to expression body"(转换成表达式函数体)和 "Convert to block body"(转换成代码块函数体)
细心的你或许注意到此处的表达式函数体也没有写出返回类型,做为一门静态类型的语言,Kotlin不是要求每一个表达式都应该在编译期具备类型吗?事实上,每一个变量和表达式都有类型,每一个函数都有返回类型。可是对表达式体函数来讲,编译器会分析做为函数体的表达式,并把它的类型做为函数的返回类型,即便没有显示得写出来。这种缝隙一般被称做类型推导。
在Java中变量的声明是从类型开始的,就像这样:
final String str = "this is final string";
int a = 12;
复制代码
可是在Kotlin中这样是行不通的,由于许多变量声明的类型均可以省略。因此在Kotlin中以关键字开始,而后是变量名称,最后能够加上类型(也能够省略):
val str: String = "this is a final string"
var a = 12
复制代码
其中: String
也是能够省略的,经过=
右边推导出左边变量的类型是String,就像a
变量省略类型。
声明变量的关键字有两个: val
(来自value)——不可变引用。在初始化以后不能再次赋值,对应Java中final修饰符。 var
(来自variable)——可变引用。这种变量的值能够被改变,对应Java中的普通变量。
虽然var
表示可变,而且如上面看到的也省略的类型,乍一看彷佛和js等脚本语言相似,能够直接赋值另外一种类型的值,好比这样:
var a = 12
a = "string"//错误
复制代码
但实际上,这样作是错误的,即便var
关键字容许变量改变本身的值,但它的类型倒是改变不了的。此处a
变量在首次赋值时就肯定了类型,这里的类型是Int
,再次赋值String
类型的值时就会提示错误,而且运行也会发生ClassCastException。
注意,尽管val
引用自身是不可变的,可是它指向的对象多是可变的,例如:
val languages = arrayListOf("Java")
languages.add("Kotlin")
复制代码
其实和Java中一致,final定义一个集合,集合中的数据是能够改变的。
###字符串模板
val name = "HuXiBing"
println("Hello, $name!")
复制代码
这是一个Kotlin的新特性,在代码中,你申明了一个变量name
,而且后面的字符串字面值中使用了它。和许多脚本语言同样,Kotlin让你能够在字符串字面值中引用局部变量,只须要在变量名称前面加上字符$
,这等价于Java中的字符串连接 "Hello, " + name + "!"
,效率同样可是更紧凑。 经过转换成Java代码,咱们能够看到这两句代码实际上是这样的:
String name = "HuXiBing";
String var3 = "Hello, " + name + '!';
System.out.println(var3);
复制代码
固然,表达式会进行静态检查,若是你试着引用一个不存在的变量,代码根本不会编译。 若是要在字符串中使用$
,你须要对它转义:println("\$x")
会打印$x
,并不会吧x
解释成变量的引用。 还能够引用更复杂的表达式,而不是仅限于简单的变量名称,只须要把表达式用花括号括起来:
println("1 + 2 = ${1 + 2}")
复制代码
还能够在双引号中直接嵌套双引号,只要它们在某个表达式的范围内(即花括号内):
val a = 12
println("a ${if (a >= 10) "大于等于10" else "小于10"}")
复制代码
先来看一个简单的JavaBean类Person,目前它只有一个属性:name。
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
复制代码
在Java中,构造方法的方法体经常包含彻底重复的代码:它把参数赋值给有着相同名称的字段。在Kotlin中,这种逻辑不用这么多的样板代码就能够表达。 使用Convert Java File To Kotlin File将这个对象转换成Kotlin:
class Person(val name: String)
复制代码
这种只有数据没有其余代码的类一般被叫作值对象,许多语言都提供简明的语法来声明它们。 注意从Java到Kotlin的转换过程当中public
修饰符消失了,在Kotlin中默认是public
,因此能够省略它。
类的概念就是把数据和处理数据的代码封装成一个单一的实体。在Java中,数据存储在字段中,一般仍是私有的。若是想让类的使用者访问到数据,得提供访问器方法:一个getter,可能还有一个setter。在Person类中你已经看到了访问器的例子。setter还能够包含额外的逻辑,包括汗蒸传给它的值、发送关于变化的通知等。 在Java中,字段和其访问器的组合经常被叫作属性,在Kotlin中,属性时头等的语言特性,彻底代替了字段和访问器的方法。在类中声明一个属性和声明一个变量同样:使用val和var关键字。声明成val的属性是只读的,而var属性是可变的。
class Person(
val name: String,//只读属性,生成一个字段和一个简单的getter
var isMarried: Boolean//可写属性:生成一个字段、一个getter、一个setter
)
复制代码
看看转换成Java的代码可能更清晰一点:
public final class Person {
@NotNull
private final String name;
private boolean isMarried;
@NotNull
public final String getName() {
return this.name;
}
public final boolean isMarried() {
return this.isMarried;
}
public final void setMarried(boolean var1) {
this.isMarried = var1;
}
public Person(@NotNull String name, boolean isMarried) {
super();
Intrinsics.checkParameterIsNotNull(name, "name");
this.name = name;
this.isMarried = isMarried;
}
}
复制代码
简单的说就是平时咱们用代码模板生成的bean,在Kotlin中连模板都不须要使用了,编译时会自动生成对应的代码。 在Java中使用应该比较熟悉了,是这个是这样的:
Person person = new Person("HuXiBing", true);
System.out.println(person.getName());
System.out.println(person.isMarried());
复制代码
生成的getter和setter方法都是在属性名称前加上get和set前缀做为方法名,可是有一种例外,若是属性时以is开头,getter不会增长前缀,而它的setter名称中is会被替换成set。因此你调用的将是isMarried()
。 而在Kotlin中使用是这样的:
val person = Person("HuXiBing", true) //调用构造方法不须要关键字new
println(person.name) //能够直接访问属性,但调用的时getter
println(person.isMarried)
复制代码
在Kotlin中能够直接引用属性,不在须要调用getter。逻辑没有变化,但代码更简洁了。可变属性的setter也是这样:在Java中,使用person.setMarried(false)
来表示离婚,而在Kotlin中,能够这样写:person.isMarried = false
。
若是getter和setter方法中须要额外的逻辑,能够经过自定义访问器的方式实现。例如如今有这样一个需求:声明一个矩形,它能判断本身是不是一个正方形。不须要一个单独的字段来存储这个信息,由于能够随时经过检查矩形的长宽是否相等来判断:
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() {//声明属性的getter
return height == width
}
}
复制代码
属性isSquare
不须要字段来保存它的值,它只有一个自定义实现的getter,它的值是每次访问属性的时候计算出来的。还记得以前的表达式函数体吗?此处也能够转成表达式体函数:get() = height == width
。 一样的看下转换成Java代码更好理解:
public final class Rectangle {
private final int height;
private final int width;
public final boolean isSquare() {
return this.height == this.width;
}
public final int getHeight() {
return this.height;
}
public final int getWidth() {
return this.width;
}
public Rectangle(int height, int width) {
this.height = height;
this.width = width;
}
}
复制代码
与Java相似,每个Kotlin文件都能以一条package
语句开头,而文件中定义的全部声明(类、函数及属性)都会被放到这个包中。若是其余文件中定义的声明也有相同的包,这个文件能够直接使用它们;若是包不相同,则须要导入它们。和Java同样,导入语句放在问价你的最前面使用关键字import
:
package com.huburt.imagepicker
import java.util.Random
复制代码
Java中的包和导入声明:
package com.huburt.imagepicker;
import java.util.Random;
复制代码
仅仅省略了;
还有点不一样的时Kotlin不区分导入是类仍是函数(是的Kotlin的函数能够单独存在,不是必定要声明在类中)。例如:
import com.huburt.other.createRandom
复制代码
com.huburt.other
是包名,createRandom
是方法名,直接定义在Kotlin文件的顶层函数。 在Java中,要把类放在和包结构相匹配的文件与目录结构中。而在Kotlin中包层级机构不须要遵循目录层级结构,可是无论怎样,遵循Java的目录布局更根据包结构把源码文件放到对应的目录中是个更好的选择,避免一些不期而遇的错误。
Kotlin中声明枚举:
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE
}
复制代码
而Java中枚举的声明:
enum Color {
RED, ORANGE, YELLOW, GREEN, BLUE
}
复制代码
这是极少数Kotlin声明比Java使用了更多关键字的例子(多了class
关键字)。Kotlin中,enum
是一个软关键字,只有当它出如今class前面是才有特殊的意义,在其余地方能够把它当作普通的名称使用,与此不一样的是,class任然是一个关键字,要继续使用名称clazz和aClass来声明变量。 和Java同样,枚举并非值得列表,能够给枚举类声明属性和方法:
enum class Color(val r: Int, val g: Int, val b: Int) {
RED(255, 0, 0), ORANGE(255, 165, 0),
YELLOW(255, 255, 0), GREEN(0, 255, 255),
BLUE(0, 0, 255);
fun rgb() = (r * 256 + g) * 256 + b
}
复制代码
枚举常量用的声明构造的方法和属性的语法与以前你看到的常规类同样。当你声明每一个枚举常量的时候,必须提供该常量的属性值。注意这个向你展现了Kotlin语法中惟一必须使用分号(;
)的地方:若是要在枚举类中定义任何方法,就要使用分号把枚举常量列表和方法定义分开。
对于Java,一般使用switch
来匹配枚举,例如这样:
public String getColorStr(Color color) {
String str = null;
switch (color) {
case RED:
str = "red";
break;
case BLUE:
str = "blue";
break;
case GREEN:
str = "green";
break;
case ORANGE:
str = "orange";
break;
case YELLOW:
str = "yellow";
break;
}
return str;
}
复制代码
而Kotlin中没有switch
,取而代之的是when
。和if
类似,when
是一个有返回值的表达式,所以咱们写一个直接返回when
表达式的表达式体函数:
fun getColorStr(color: Color) =
when (color) {
Color.RED -> "red"
Color.ORANGE -> "orange"
Color.YELLOW -> "yellow"
Color.GREEN -> "green"
Color.BLUE -> "blue"
}
//调用方法
println(getColorStr(Color.RED))
复制代码
上面的代码根据传进来的color值找到对应的分支。和Java不同,你不须要在每一个分支都写上break
语句(在Java中遗漏break一般会致使bug)。若是匹配成功,只有对应的分支会执行,也能够把多个值合并到同一个分支,只须要逗号(,
)隔开这些值。
fun getColorStr(color: Color) =
when (color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "yellow"
Color.GREEN -> "neutral"
Color.BLUE -> "cold"
}
复制代码
若是以为写了太多的Color
,能够经过导入的方式省略:
import com.huburt.other.Color //导入类
import com.huburt.other.Color.* //导入枚举常量
fun getColorStr(color: Color) =
when (color) {
RED, ORANGE, YELLOW -> "yellow" //直接使用常量名称
GREEN -> "neutral"
BLUE -> "cold"
}
复制代码
Kotlin中的when
结构比Java中switch
强大的多。switch
要求必须使用常量(枚举常量、字符串或者数字字面值)做为分支条件。而when
容许使用任何对象。咱们使用这种特性来写一个函数来混合两种颜色:
fun mix(c1: Color, c2: Color) {
when (setOf(c1, c2)) {
setOf(Color.RED, Color.YELLOW) -> Color.ORANGE
setOf(Color.YELLOW, Color.BLUE) -> Color.GREEN
else -> throw Exception("Dirty color")
}
}
复制代码
setOf
是Kotlin标准函数库中一个方法,用于建立Set集合(无序的)。 when
表达式把setOf(c1, c2)
生成的set集合依次和全部的分支匹配,直到某个分支知足条件,执行对应的代码(返回混合后颜色值或者抛出异常)。 能使用任何表达式做为when
的分支条件,不少状况下会让你的代码既简洁又漂亮。
你可能意识到上面的例子效率多少有些低。没此调用这个函数的时候它都会建立一些Set实例,仅仅是用来检查两种给定的颜色是否和另外两种颜色匹配。通常这不是什么大问题,可是若是这个函数调用很频繁,它就很是值得用另外一种方式重写。来避免建立额外的垃圾对象。
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == Color.RED && c2 == Color.YELLOW) ||
(c1 == Color.YELLOW && c2 == Color.RED) -> Color.ORANGE
(c1 == Color.BLUE && c2 == Color.YELLOW) ||
(c1 == Color.YELLOW && c2 == Color.BLUE) -> Color.GREEN
else -> throw Exception("Dirty color")
}
复制代码
若是没有给when
表达式提供参数,分支条件就是任意的布尔表达式。mixOptimized
方法和上面那个例子作了如出一辙的事情,这种写法不会穿件额外的对象。
在Java中常常会有这样一种情形:用父类申明引用一个子类对象,当要使用子类的某个方式时,须要先判断是不是哪一个子类,若是是的话在强转成子类对象,调用子类的方法,用代码的话就是以下的状况:
class Animal {
}
class Dog extends Animal {
public void dig() {
System.out.println("dog digging");
}
}
Animal a = new Dog();
if (a instanceof Dog) {
((Dog) a).dig();
}
复制代码
在Kotlin中,编译器会帮你完成强转的工做。若是你检查过一个变量是某种类型,后面就不须要转换它,就能够把它当作你检查过的类型使用(调用方法等),这就是智能转换。
val d = Animal()
if (d is Dog) {
d.dig()
}
复制代码
这里is
是检查一个变量是不是某种类型(某个类的实例),至关于Java中的instanceof
。能够看到d
变量是一个Animal
对象,经过is
判断是Dog
后,无需强转就能调用Dog
的方法。 智能转换只在变量通过is
检查且且以后再也不发生变化的状况下有效。当你对一个类的属性进行智能转换的时候,这个属性必须是一个val
属性,并且不能有自定义的访问器。不然,每次对属性的访问是否都能返回相同的值将无从验证。
在Kotlin中用as
关键字来显示转换类型(强转):
val d = Animal()
val dog = d as Dog
复制代码
ps:其实只是省略强转代码,我的感受做用不是很明显。
Kotlin和Java中if
有什么不一样,以前已经提到过了。如if
表达式用在适用Java三元运算符的上下文中:if (a > b) a else b
(Kotlin)和a > b ? a : b
(Java)效果同样。Kotlin没有三元运算符,由于if
表达式有返回值,这一点和Java不一样。 对于较少的判断分支用if
没有问题,可是较多的判断分支则用when
是更好的选择,有相同的做用,而且都是表达式,都有返回值。
上面的例子知足条件的分支执行只有一行代码,但若是某个分支中代码不止一行还如何处理那?固然是把省略的{}
加上做为代码块啦:
val a = 1
val b = 2
var max = if (a > b) {
println(a)
a
} else b
var max2 = when {
a > b -> {
println(a)
a
}
else -> b
}
复制代码
代码块中最后一个表达式就是结果,也就是返回值。 对比Java的代码:
int a = 1;
int b = 2;
int max;
if (a > b) {
System.out.println(a);
max = a;
} else {
max = b;
}
//没法使用switch
复制代码
少了赋值操做,而且when
的使用在多条件的状况下也更方便。是否是慢慢发现Kotlin的美妙了?
Kotlin中while
和do-while
循环与Java彻底一致,这里再也不过多叙述。
Kotlin中有区间的概念,区间本质上就是两个值之间的间隔,这两个值一般是数字:一个起始值,一个结束值,使用..
运算符来表示区间:
val oneToOne = 1 .. 10
复制代码
注意Kotlin的区间是包含的或者闭合的,意味着第二个值始终是区间的一部分。若是不想包含最后那个数,可使用函数until
建立这个区间:val x = 1 until 10
,等同于val x = 1 .. 9
你能用整数区间作的最基本的事情就是循环迭代其中全部的值。若是你能迭代区间中全部的值,这样的区间被称做数列。 咱们用整数迭代来玩Fizz-Buzz游戏。游戏玩家轮流递增计数,遇到能被3整除的数字就用单词fizz代替,遇到能被5整除的数字则用单词buzz代替,若是一个数字是3和5的公倍数,你得说FizzBuzz。
fun fizzBuzz(i: Int) = when {
i % 15 == 0 -> "FizzBuzz"
i % 5 == 0 -> "Buzz"
i % 3 == 0 -> "Fizz"
else -> "$i"
}
fun play() {
for (i in 1..100) {
print(fizzBuzz(i))
}
}
复制代码
Kotlin中for
循环仅以惟一一种形式存在,其写法:for <item> in <elements>
。 区间1 .. 100
也就是<elements>
,所以上面这个例子遍历了这个数列,并调用fizzBuzz方法。 假设想把游戏变得复杂一点,那咱们能够从100开始倒着计数,而且只计偶数。
for (i in 100 downTo 1 step 2) {
print(fizzBuzz(i))
}
复制代码
这里100 downTo 1
是递减的数列(默认步长是1),而且设置步长step
为2,表示每次减小2。
咱们用一个打印字符二进制表示的小程序做为例子。
val binaryReps = TreeMap<Char, String>()//使用TreeMap让键排序
for (c in 'A'..'F') {//使用字符区间迭代从A到F之间的字符
val binary = Integer.toBinaryString(c.toInt())//吧ASCII码换成二进制
binaryReps[c] = binary//根据键c把值存入map
}
for ((letter, binary) in binaryReps) {//迭代map,把键和值赋给两个变量
println("$letter = $binary")
}
复制代码
..
语法不只能够建立数字区间,还能够建立字符区间。这里使用它迭代从A到F的全部字符,包括F。 for循环容许展开迭代中集合的元素(map的键值对),把展开的结果存储到两个独立的变量中:letter是键,binary是值。 map中能够根据键老访问和更新map的简明语法。使用map[key]
读取值,并使用map[key] = value
设置它们,而不须要地爱用get和put。这段binaryReps[c] = binary
等价于Java中的binaryReps.put(c, binary);
你还可使用展开语法在迭代集合的同时跟踪当前项的下标。不须要建立一个单独的变量来存储下标并手动增长它:
val list = arrayListOf("10", "11", "1001")
for ((index, element) in list.withIndex()) {
println("$index : $element")
}
复制代码
使用in
运算符来检查一个值是否在区间中,或者它的逆运算!in
来检查这个值是否不在区间中。下面展现了如何使用in
来检查一个字符是否属于一个字符区间。
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'z'
fun isNotDigit(c: Char) = c !in '0'..'9'
复制代码
这种检查字符是不是英文字符的技巧看起来很简单。在底层,没有什么特别处理,依然会检查字符的编码是否位于第一个字母编码和最后一个字母编码之间的某个位置(a <= c && c <= z
)。可是这个逻辑被简洁地隐藏到了标准库中的区间类实现。 in
运算符合!in
也适用于when
表达式。
fun recognize(c: Char) = when (c) {
in '0'..'9' -> "It's a digit!"
in 'a'..'z', in 'A'..'z' -> "It's a letter!"
else -> "I don't know..."
}
复制代码
Kotlin的异常处理语句基本形式与Java相似,除了不须要new
关键字,而且throw
结构是一个表达式,能做为另外一个表达式的一部分使用。
val b = if (a > 0) a else throw Exception("description")
复制代码
和Java同样,使用带有catch
和finally
子句的try
结构来处理异常,下面这个例子从给定的文件中读取一行,尝试把它解析成一个数字,返回这个数字;或者当这一行不是一个有效数字时返回null
。
fun readNumber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: NumberFormatException) {
return null
} finally {
reader.close()
}
}
复制代码
Int?
表示值多是int类型,也可能null,Kotlin独特的null机制,不带?
标识的声明没法赋值null,在以后的文章中会具体介绍。
和Java最大的区别就是throws
子句没有出如今代码中:若是用Java来写这个函数,你会显示地在函数声明的后写上throws IOException
。你须要这样作的缘由是IOException
是一个受检异常。在Java中,这种异常必须显示地处理。必须申明你的函数能抛出的全部受检异常。若是调用另一个函数,须要处理这个函数的受检异常,或者声明你的函数也能抛出这些异常。 和其余许多如今JVM语言同样,Kotlin并不区分受检异常和未受检异常。不用指定函数抛出的异常,并且能够处理也能够不处理异常。这种设计是基于Java中使用异常实践作出的决定。经验显示这些Java规则经常致使许多毫无心义的从新抛出或者忽略异常的代码,并且这些规则不能老是保护你免受可能发生的错误。
Kotlin中try
关键字就像if和when同样,引入了一个表达式,能够把它的值赋给一个变量。例如上面这个例子也能够这样写:
fun readNumber(reader: BufferedReader): Int? =
try {
val line = reader.readLine()
Integer.parseInt(line)
} catch (e: NumberFormatException) {
null
} finally {
reader.close()
}
复制代码
若是一个try
代码块执行一切正常,代码块中最后一个表达式就是结果。若是捕获到了一个异常,相应的catch
代码块中最后一个表达式就是结果。