Kotlin语法基础(一)

306 阅读3分钟

一、第一个Hello World

熟悉Kotlin从hello world开始

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

我们来简单对比一下Java程序

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

可以看到

  • 1、kotlin的方法是可以不写在类中,而Java的每个方法都是需要写在类中的.
  • 2、相比较Javamain方法,kotlinmain方法可以不加入任何入参
  • 3、同样都是打印输出,Kotlinprintln方法调用起来也更加简单,同时,我们注意到,Kotlin的语法是不需要分号的.

二、方法调用

2.1 普通方法

接下来我们尝试写几个普通方法.

fun printMessage(message: String): Unit {
    println(message)
}

fun printMessageWithPrefix(message: String, prefix: String = "Info") {
    println("[$prefix] $message")
}

fun sum(x: Int, y: Int): Int {
    return x + y
}

fun multiply(x: Int, y: Int) = x * y


fun main() {
    printMessage("Hello")
    printMessageWithPrefix("Hello", "Log")
    printMessageWithPrefix("Hello")
    printMessageWithPrefix(prefix = "Log", message = "Hello")
    println(sum(1, 2))
    println(multiply(3, 4))
}
Hello
[Log] Hello
[Info] Hello
[Log] Hello
3
12

可以看到

  • 1、kotlin的方法中,参数后面通过冒号:声明参数类型
  • 2、kotlin的返回中,返回Unit或者无返回则代表没有返回值,在方法的后面通过冒号:声明返回类型,return相应类型的参数,或者直接通过=返回相应的返回值
  • 3、在kotlin的方法中,我们通过参数="xx"声明方法中入参的默认值,在引用方法时,可以缺省该入参,使用默认值
  • 4、当我们调用方法按照默认顺序传值时,入参的参数名可以缺省.而如果我们想要调换参数顺序,可以通过指定参数名赋值调换方法内的参数顺序.

我们通过tools-show Kotlin bytecode

image.png

再点击右侧的decompile可以看看kotlin的java代码

public final class MyTestKt {
   public static final void printMessage(@NotNull String message) {
      Intrinsics.checkNotNullParameter(message, "message");
      System.out.println(message);
   }

   public static final void printMessageWithPrefix(@NotNull String message, @NotNull String prefix) {
      Intrinsics.checkNotNullParameter(message, "message");
      Intrinsics.checkNotNullParameter(prefix, "prefix");
      String var2 = '[' + prefix + "] " + message;
      System.out.println(var2);
   }

   // $FF: synthetic method
   public static void printMessageWithPrefix$default(String var0, String var1, int var2, Object var3) {
      if ((var2 & 2) != 0) {
         var1 = "Info";
      }

      printMessageWithPrefix(var0, var1);
   }

   public static final int sum(int x, int y) {
      return x + y;
   }

   public static final int multiply(int x, int y) {
      return x * y;
   }

   public static final void main() {
      printMessage("Hello");
      printMessageWithPrefix("Hello", "Log");
      printMessageWithPrefix$default("Hello", (String)null, 2, (Object)null);
      String var0 = "Hello";
      String var1 = "Log";
      printMessageWithPrefix(var0, var1);
      int var2 = sum(1, 2);
      System.out.println(var2);
      var2 = multiply(3, 4);
      System.out.println(var2);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

可以看到

  • 1、对于multiply的方法调用没有生命返回类型时,java代码中是推断该方法返回值为int
  • 2、 printMessageWithPrefix$default("Hello", (String)null, 2, (Object)null);

当用户的方法参数缺省时,并没有如我们预想中进行了重载,而是直接创建了一个不同名字的方法,直接改了调用的地方.

  • 3、String var0 = "Hello"; String var1 = "Log"; printMessageWithPrefix(var0, var1); 当我们手动通过声明参数值调整了参数的前后顺序后,java代码又帮我们将顺序改了回来

2.2 infix

我们在使用一些系统类时,通常需要给某些系统类扩展一些额外的方法,此时我们使用infix关键字可以方便的实现我们的需求.

fun main() {
    infix fun Int.times(str: String) = str.repeat(this)
    println(2 times "Bye")


    val pair = "Ferrari" to "Katrina"
    println(pair)

    infix fun String.onto(other: String) = Pair(this, other)
    val myPair = "McLauren" onto "Lucas"
    println(myPair)

    val sophia = Person("Sophia")
    val claudia = Person("Claudia")
    sophia likes claudia


}

class Person(val name: String) {
    private val likedPeople = mutableListOf<Person>()
    infix fun likes(other: Person) {
        println("添加前的---$likedPeople")
        likedPeople.add(other)
        println("添加后的---$likedPeople")
    }
}
ByeBye
(Ferrari, Katrina)
(McLauren, Lucas)
添加前的---[]
添加后的---[com.example.kotlintest.Person@1e67b872]

  • 1、可以看到,我们在Int类中定义了一个times方法,用于重复次数的返回字符串入参.
  • 2、在String方法中又定义了一个onto方法,将入参的String字符串使用Pair进行组装
  • 3、我们也可以使用中缀符号定义成成员方法,用于类对象使用.

2.3 操作符方法

有些方法还可以升级为操作符方法,允许调用者使用相应的操作符进行计算,达到同样的效果.

operator fun Int.times(str: String) = str.repeat(this)
println(2 * "Bye")


operator fun String.get(range: IntRange) = substring(range)
val str = "operator fun String.get(range:IntRange)=substring(range)"
println(str[0..14])
ByeBye
operator fun St
  • 1、操作符和对应的重载方法是一一对应的,times方法对应*
  • 2、times对应了*,2 * "Bye" 相当于调用了 2.times("Bye")
  • 3、 String.get(range:IntRange)-->str[0..14]

2.4 带有vararg参数的方法.

vararg就相当于java中的String... args 一个较长串的入参

fun printAll(vararg messages: String) {
    for (m in messages) println(m)
}

printAll("Hello", "Hallo", "Salute", "Hole", "你好")

fun printAllWithPrefix(vararg messages: String, prefix: String) {
    for (m in messages) println(prefix + m)
}

printAllWithPrefix("Hello", "Hallo", "Salute", "Hole", "你好", prefix = "Greeting:")

fun log(vararg entries: String) {
    printAll(*entries)
}

log("Hello", "Hallo", "Salute", "Hole", "你好")
Hello
Hallo
Salute
Hole
你好
Greeting:Hello
Greeting:Hallo
Greeting:Salute
Greeting:Hole
Greeting:你好
Hello
Hallo
Salute
Hole
你好
  • 1、可变参数列表vararg的长度可以是0个或者任意数量
  • 2、在java中,含有可变参数的方法中可变参数需要放在方法参数的末尾,而kotlin中可以做到相应参数指定赋值,因此我们在命名方法时可以将可变参数不必限制在末尾.
  • 3、vararg 是以数组的形式传递到参数中,这和java是一样的。你可以用* 将可变长度的参数以可变长度的参数来传递而不是以数组的方式来传递。这里不加 * 将会报错

2.5 变量

kotlin具有强大的变量的推断能力.

你可以明确的定义变量的类型,也可以让编译器自己来做变量类型推断.

val关键词修饰的变量,表明这是不变的变量.

var关键词修饰的变量,表明这是可变变量.

var a: String = "initial"
a = "SChange"
println(a)

val b: Int = 1
val c = 3

在上述代码中,我们定义了可变变量a,并且对a进行了初始化的赋值,随后又进行了一轮新的赋值.

随后我们声明了不可变变量b,val的声明类似于java中的final定义,之后再对其更改都会报错.

在定义不可变量c时,此时没有明确定义变量类型.类型将由编译器推断,这里编译器推断的是Int

var e: Int
println(e)

上述代码定义了一个变量并且没有初始化,此时打印并访问该变量,代码在编译时就会报错,因为变量没有初始化.

kotlin可以在read变量之前对这个变量进行初始化

val d: Int
var ss: Boolean = false
if (ss) {
    d = 1222
} else {
    d = 22
}
println(d)

对于不可变变量,我们也可以进行延后初始化,确保在对变量进行初始化之后再读取.

2.6 空安全检查

我们知道,java的崩溃很多都是空指针,kotlin通过禁止给变量赋值Null值想要减少NullPointerException的发生.

如果在运行过程中可能给变量赋予空值,可以在类型名称之后加上?

fun main() {
    var neverNull: String = "这是一个非空变量"//对非空变量赋值

    neverNull = null //此时赋空值会报错

    var nullable: String? = "这是一个可空变量"
    nullable = null //将可空变量设置为null,此时不会报错

    var inferredNonNull = "The compiler assumes non-null"//定义一个没有声明变量类型的变量inferredNonNull
    inferredNonNull = null//可见编译器直接将该变量推断为非空变量,此时再赋值为null会报错

    strLength(neverNull)//获取neverNull字符串长度

    strLength(nullable)///获取nullable字符串长度,此时会直接报错,不可以将非空变量作为入参调用
}

fun strLength(notNull: String): Int {//定义了一个非空入参方法,获取字符串长度
    return notNull.length
}

对于可能是空值入参的方法,我们可以自己做个空值判断.

fun describeString(maybeString: String?): String {
    return if (maybeString != null && maybeString.isNotEmpty()) {
        "String of length ${maybeString.length}"
    } else {
        "Empty or null String"
    }
}

2.7 类

  • kotlin中的类定义由类名,类头(包含类型声明,私有构造方法等)和类体组成,并且由大括号组成.

  • 类头和类体都是可以缺省的,如果类体缺省,那么大括号也可以缺省

class Customer

class Contact(val id: Int, var email: String)

fun main() {
    val customer = Customer()

    println(customer)

    val contact = Contact(1, "mary@gmail.com")

    println(contact.id)
    contact.email = "jane@gmail.com"
}
  • 在上面的代码中,我们定义了一个Customer类,这个类中没有定义任何属性和构造方法,可以看到在main()方法中,通过Customer()我们也声明了一个Customer变量,kotlin将自动给这个类赋予一个无参构造方法
  • 在Contact类中,我们还给该类的构造方法声明了一个不可变变量的id,一个可变的变量email,在main()方法中,我们也可以通过携带这两个入参声明Contact变量

2.8 泛型

泛型是现代语言都具有的一种模版方法.泛型类和泛型方法使得代码具有高度的可复用性.

2.8.1 泛型类

class MutableStack<E>(vararg items: E) {
    private val elements = items.toMutableList()

    fun push(element: E) = elements.add(element)

    fun peek(): E = elements.last()

    fun pop(): E = elements.removeAt(elements.size - 1)

    fun isEmpty() = elements.isEmpty()

    fun size() = elements.size

    override fun toString(): String = "MutableStack(${elements.joinToString()}}"
}

可以看到,上述代码定义了一个泛型类,E被称为泛型参数.在泛型类里面的方法中可以用E来代替泛型参数,也可以用E来代替返回值.

2.8.2 泛型方法

除了定义泛型类之外,我们还可以自定义泛型方法

fun <E> mutableStackOf(vararg elements: E) = MutableStack(*elements)

fun main() {

    val stack = mutableStackOf(0.62, 3.14, 2.7)
    println(stack)

}

编译时将会推断出mutableStackOf是一个Double类型的泛型入参,所以我们不需要再显式的声明mutableStackOf(...)

2.9 继承

kotlin支持传统的面向对象的继承机制

open class Dog {
    open fun sayHello() {
        println("wow wow!")
    }
}

class Yorkshire : Dog() {
    override fun sayHello() {
        println("wif wif")
    }
}

fun main() {
    val dog: Dog = Yorkshire()
    dog.sayHello()
}
  • 1、Kotlin的类默认是final的,如果你要允许类可以被继承,需要添加open关键字到class之前
  • 2、kotlin的方法默认也是final的,如果你要允许方法可以被重写,需要添加open关键字到fun之前
  • 3、一个子类应该定义在:SuperclassName()之前,空的()意味着super()了父类的默认构造方法.
  • 4、重写的方法需要添加override关键字

2.9.1 带有参数的构造方法的继承

open class Tiger(private val origin: String) {
    fun sayHello() {
        println("A tiger from $origin says: greetingHello!")
    }
}

class SiberianTiger : Tiger("Siberia")

fun main() {
    val tiger: Tiger = SiberianTiger()
    tiger.sayHello()
}

如果想要用父类的构造方法定义一个子类,那么需要在定义的时候传入传给父类的字段

2.9.2 传递参数给父类

open class Lion(val name: String, private val origin: String) {
    fun sayHello() {
        println("$name,the lion from $origin says:gra oh!")
    }
}

class Asiatic(name: String) : Lion(name = name, origin = "India")

fun main() {
    val lion: Lion = Asiatic("Rufo")
    lion.sayHello()
}
  • 1、可以看到Asiatic中的name既没有声明成val 也没有主动声明成var,只是用来将值传递给父类的构造方法

  • 2、用构造参数Rufo创建一个Asiatic的实例,这个调用将会调用父类Lion的双参构造方法.