【笔记】Java和Kotlin对比
Java独有
受检异常
例如
public interface Appendable {
Appendable append(CharSequence csq) throws IOException;
}
使用时必须捕获异常
try{
stringBuilder.append("")
}catch(ioException:IOException){
}
这种检查异常的机制的问题在于,会出现很多中间API不想关注如何处理这些异常,要么继续抛出,要么吞掉异常。继续抛出毫无疑问是代码量的冗余,吞掉更是打断了异常的传递,所以在大型项目中,会导致代码量增加,但是代码质量并不会提高。
当然如果实在想要在Kotlin声明一个受检异常,可以使用@Throws注解。Java中调用时和正常的编译期异常一样处理。
不是类的原生类型
Java中有八种原生类型,也叫基本类型。
具体可见[[Java基本数据类型]]。
kotlin采用包装类型,提供更一致更安全的编程体验,通过包装类型,kotlin会在需要的时候自动装箱和拆箱,代码更加易于维护,此外包装类型还方便提供空安全性,扩展函数等功能。
静态成员
static关键字修饰的静态变量和静态方法,归属于整个类。
在加载类的过程中就完成了静态变量的内存分配。
在类内部,任何方法都可以直接访问,类外部可以通过类名访问到非私有的静态变量。
kotlin中, 以 伴生对象、 顶层函数、 扩展函数 或者 @JvmStatic 取代。
简短说明:
伴生对象
kotlin中声明的companion object。
顶层函数
直接在文件顶层声明,不需要创建一个类来保存。
扩展函数
class A(){
}
//为A类添加扩展函数b
fun A.b(){
println("b")
}
Java中使用是显式传入一个a对象
A a = new A();
${扩展函数文件名Kt}.b(a);
通配符类型
这里指的是Java泛型的通配符。
为了满足:
- 从一个泛型集合里面读取元素
- 往一个泛型集合里面插入元素
有三种方式定义一个使用泛型通配符的集合
无限定通配符
也叫无界通配符
//声明一个变量
List<?> listTestA = new ArrayList<>();
//作为参数类型
public void processElements(List<?> elements){
for(Object o : elements){
Sysout.out.println(o);
}
}
表示该集合是一个可以持有任意类型的集合,因为不明确的类型,所以只能进行读操作。并且只能把读取到的元素当成Object实例对待。
感觉主要作用就是给一些通用函数作为参数的类型声明时使用。
上界通配符
class A{}
class B extends A{}
class C extends B{}
List<A> a = new ArrayList<A>();
List<B> b = new ArrayList<B>();
List<C> c = new ArrayList<C>();
processElements(a);
processElements(b);
processElements(c);
public static void processElements(List<? extends A> list){
for (Object o : list){
System.out.println(o);
}
}
上界指的就是最顶层的类,可以持有此类的子类,以及子类的子类等继承类,支持的类型仅和上界相关。由于依旧不能确定具体的类型,所以仍然只读。
下界通配符
public static void insertElements(List<? super C> list){
for (Object o : list){
System.out.println(o);
}
}
insertElements(a);
insertElements(b);
insertElements(c);
参数化的类型可能是指定的类型,或者此类型的父类,直到Object。
通配符使用注意
使用通配符主要为了在泛型方法中提供一定的参数类型限制和灵活性。
上界通配符更多限制集合的类型范围,而下界通配符允许添加指定类型的子类元素,所以更灵活一些。
由于可能导致类型信息的丢失或者引发编译错误,所以一定要仔细做好类型匹配和设计。
kotlin
Kotlin另外设计了一套泛型子类管理系统,详情见下面的声明处型变&类型投影
总结:
Java通过通配符处理泛型类型和子类型关系。kotlin通过声明处型变和类型投影进一步控制泛型的生产者和消费者使用。
三目操作符
a ? b : c
kotlin中使用if表达式取代。
Kotlin独有
lambda表达式+内联函数
个人认为:lambda表达式简化函数声明和使用,方便高阶函数利用。
内联化lambda表达式可以消除函数作为对象时的开销。
tips:内联函数可能会造成代码膨胀,增加编译后的代码大小。
所以注意使用noinline关键字,局部函数,kotlin标准库函数避免导致内联大过函数的问题发生。
扩展函数
这个上面提到Java静态成员时也讲过,主要需要注意几个地方:
扩展是静态解析的
并没有在类中插入新成员!
表现为:子类和父类有相同的扩展函数时,调用声明为父类的子类对象的扩展函数,实际执行的是父类的扩展。
并且不能和类型的成员函数完全相同,否则总是取成员函数。
扩展属性
扩展属性不能有初始化器,因为并没有将成员插入类中。所以实际上,哪怕是同一个对象,每次调用他的扩展属性,都是不同的执行过程。
所以扩展属性的行为只能由显式的getters/setters定义。
伴生对象扩展
如果类已经定义了伴生对象,则可以为其定义扩展函数和属性。
class MyClass {
companion object { } // 将被称为 "Companion"
}
fun MyClass.Companion.printCompanion() {
println("companion")
}
扩展的作用域
一般来说都是在顶层定义,直接在包里。
但是如果在类里定义,那仅可在该类中使用,无法在其他类中访问到。
空安全
在Kotlin中发生NullPointerException的几种可能
- 显式调用throw NullPointerException
- 使用!!操作符
- 初始化顺序导致的泄漏this和超类构造函数调用开放成员。
- Java互操作
- 访问[[平台类型]]的null引用成员
- 可空的Java对象用于非空Kotlin代码中
- 外部Java代码引发其他问题。
对于第三的一些补充:
要避免初始化导致的空安全问题,主要在注意初始化顺序,以及父类,子类初始化时的时机。
下面是一种可能的情况:
class NullTestClass {
fun throwNull(){
try {
/**
* 由于SuperClass构造函数会先于SubClass执行
* 所以调用会先调用printValue然后再创建value,value此时未初始化
* 然后因为SubClass重写printValue函数,导致实际调用了一个不可空的命令
* SubClass对象一旦创建就处罚空异常。
*/
val subClass = SubClass()
}catch (e:NullPointerException){
println("由于构造函数调用了开放的未初始化的值导致空异常")
}
}
}
abstract class SuperClass(){
abstract var value : String
init {
printValue()
}
open fun printValue(){
println("SuperClass打印value值为${value}")
}
}
class SubClass(): SuperClass() {
override var value : String = "init"
override fun printValue(){
println("SubClass打印value的长度为${value.length}")
}
}
Elvis操作符
可以用Elvis操作符表达空判断,或者检查函数参数:
val l = b?.length ?: -1
val name = student.name ?: throw IllegalArgumentException("name expected")
非空断言
通过!!操作符(非空断言运算符)强制转为非空类型。
可空类型转换
用as?可以达到安全的类型转换,不成功会返回null。
智能类型转换
用is代替java的instanceof,并且取反逻辑也简单!is。
为什么叫智能,因为编译器会跟踪不可变值的is检测,以及显式转换。
fun demo1(x: Any) {
if (x is String) {
print(x.length) // x 自动转换为字符串
}
}
fun demo2(x:Any){
if (x !is String) return
print(x.length) // x 自动转换为字符串
}
这种智能检测在包括:if,when,while,&&,||等地方都可以发挥作用。
智能转换需要遵循下面的规则:
val:基本都行,除开局部委托属性,open属性,自定义getter属性。
var:只有是局部变量,且在检测和使用间没有修改才行。
as:不安全的转换操作符。因为转换不可能时会抛出异常。
as?:安全的转换操作符,但是失败时会返回null。
字符串模版
以$开头的模版表达式
//支持表达式,函数,对象属性
fun print(){
val value = 10
println("Test $value & ${value+10} & ${value.minutes}")
}
属性
kotlin的属性可以用关键字var声明为可变的,也可以用关键字val声明为只读的。
只读的属性不可setter。
有幕后字段(field)和幕后属性的说法。
只读属性编译器已知,用const修饰符标记为编译期常量。
在类体中的可变属性(kotlin 1.2中顶层属性和局部变量也可以)可以用lateinit修饰,表明属性会延迟初始化。
这个属性不能是声明在主构造函数中,不能为空,不能是原生类型。
//主构造函数中的属性不能lateinit
//class PropertyTestClass(lateinit var a : String) {
class PropertyTestClass() {
//原生类型,可空也不能lateinit
// lateinit var b : Int?
lateinit var c : String
fun test(){
c = ""
lateinit var d : String
d = ""
e = ""
/**
* 通过属性引用::
* 使用.isInitialized可以获取lateinit属性是否已经初始化
*/
if (::c.isInitialized){
}
}
}
lateinit var e : String
主构造函数
kotlin类可以有一个主构造函数和多个次构造函数。主构造函数跟在类名后面。constructor这个关键字在主构造函数没有任何注解或者可见性修饰符时可以省略。
主构造函数需要的初始化代码放到以init关键字为前缀的初始化块中。
可以有多个初始化块,执行顺序和属性初始化器一样按类体中的顺序执行。

可以看到打印顺序和类体中的顺序一致。
类体中可以声明以constructor为前缀的次构造函数。用this可以委托到另一个构造函数。
所有次构造器都会隐式委托到主构造器,导致初始化块(init)的代码依旧先执行。

委托
主要就是属性委托和接口委托。
[[Kotlin委托]]
变量和属性类型的类型判断
会通过上下文进行推断。
单例
通过对象声明(object)非常简单的完成单例声明。初始化过程是线程安全的并且在首次访问时进行。
同时这也相当于java中的静态类型。
object ObjectTestClass1{
fun print(){
println("ObjectTestClass1")
ObjectTestClass2.print()
}
//对象声明可以嵌套到其他对象声明或非内部类中
object ObjectTestClass2{
fun print(){
println("在其他对象声明中嵌套对象声明ObjectTestClass2")
}
}
}
声明处型变&类型投影
open class A {}
class B : A(){}
//可以在泛型声明的地方指定类型的协变(out),逆变(in)
abstract class C<out T,in V>{
abstract fun output():T
fun v(v:V){}
}
声明处型变主要可以避免类似于List<? extends Object>这类无意义的写法,直接可以在使用处使用子类型。
类型投影
在使用处声明out和in,类似于Java中的上下界通配符。这种也叫使用处型变。
//相当于<? extends Object>,保证from和to都只读。
fun copy(from: Array<out Any>, to: Array<out Any>) { …… }
//相当于<? super String>,传入`CharSequence` 数组或一个 `Object` 数组都可以
fun fill(dest: Array<in String>, value: String) { …… }
星投影
使用处,如果不关心对应类型,或者只需要共性的操作,可以用星投影替代部分写法。
使用时:单个类型参数时Foo<*>,每个参数类型都可以单独投影Function<*,*>。
| 原写法 | 具体含义 |
|---|---|
Foo<out TUpper> |
生产者是T? |
Foo<in Nothing> |
泛型T未知,没有安全方法写入 |
kotlin的型变和Java的上下界
Kotlin的型变,给我的感觉是针对声明泛型的类来说,该泛型可以用在消费者(参数类型)上还是生产者(返回类型)上。
而Java的上界和下界,是对使用泛型的方法,提供两种不同的参数类型限定。
所以实际使用的时候,kotlin中应该没有类似对应下界通配符的东西。倒是out有部分功能和上界通配符比较相似,但是out在声明处有指定生产者的功能。
这里指的是声明处型变,如果是使用处协变倒是有类似上下界通配符的效果的。比如
fun test(from : Array<in Int>,to : Array<out Number>){
}
泛型约束:
kotlin中泛型没有下界限定符,但是可以通过T:${class}的方式实现上界通配符。
这不属于型变,所以我们可以说kotlin中的协变和逆变提供了一种更灵活和安全的方式处理泛型和其子类型关系。
区间表达式
rangeTo函数以及其操作符形式..创建两个值的区间。
主要记住几个点就行:
- 一般配合
in和!in在for循环中使用 - 反向区间是downTo
- 调整步长是step,不能为负
- 整数区间等于数列,可以用集合函数
val range : IntRange = 0..4
//辅以in 或 !in函数
//整数类型区间可以进行迭代
for (i in range){
println("$i")
}
//反向迭代用downTo
val downRange = 4 downTo 0
操作符重载
[[Kotlin操作符重载]]
伴生对象
类似Java的类的静态成员。无需一个类的实例,只需要用类名即可调用。
数据类
只保存数据的类,编译器自动从主构造函数中声明的属性中导出
- equals&hashCode
- toString,格式是"dataClassName(propertyName1=1,propertyName2=false)"
- componetN函数,解构声明,按声明顺序
- copy函数
数据类的一些要求
- 主构造函数至少一个参数,并且标记为val/var
- 不能是抽象,开放,密封或者内部的
- 数据类只能实现接口(1.1以前)
在成员继承方面
- 显式实现equals,hashCode,toString函数或者父类有final实现,则不会自动生成
- 超类有open的componetN函数并且返回兼容类型才会为数据类生成相应函数,否则无法覆盖,会报错
- 超类不能是:已经有copy函数并且签名匹配
- 不允许为
componentN()以及copy()函数提供显式实现。
自动生成的函数,只针对主构造器中定义的属性,在类体中的属性不会被考虑。
优点:数据类自动声明componetN函数,可以很方便的使用解构声明完成变量声明和使用。自动声明的copy,toString,equals和hashcode也很方便。
分离用于只读和可变集合的接口
kotlin中有可以型变的只读集合,以及不能型变的可变集合。

函数类型
主要就是下面几点:
- 返回值取最后一行表达式结果
- 多参数时写法:
- 单参数可以用it:
- 额外接收者类型:String.(Int)->Unit
- 挂起函数:suspend A.(B)->C
- 函数别名
- 调用函数
::
[[kotlin中缀函数]]
协程
kotlin提供语言级协程支持,方便异步编程。
Kotlin相对Java的优点
- 空安全对应的类型控制系统,T和T?
- 无原始类型
- kotlin数组不型变,防止可能的运行失败
- 函数类型代替SAM转换
- 使用处型变代替通配符
- 没有受检异常,减少多余代码。
Thanks:
与 Java 语言比较
Java static关键字(静态变量和静态方法)
一文读懂Java泛型中的通配符 ?
聊一聊-JAVA 泛型中的通配符 T,E,K,V,?

浙公网安备 33010602011771号