Kotlin是一种现代编程语言,可编译为Java字节码。 它是免费的, 开放 源码 ,并承诺使编码为Android更有趣。
在上一篇文章中 ,您了解了Kotlin中的类和对象。 在本教程中,我们将继续学习有关属性的更多信息,并通过探索以下内容来研究Kotlin中高级的类类型:
- 后期初始化的属性
- 内联属性
- 扩展属性
- 数据,枚举,嵌套和密封类
1.后期初始化属性
我们可以在Kotlin中将非null属性声明为late-initialized 。 这意味着非null属性不会在声明时使用值初始化-实际的初始化不会通过任何构造函数发生-而是会通过方法或依赖项注入来后期初始化。
让我们看一个例子来了解这个独特的属性修饰符。
class Presenter {
private var repository: Repository? = null
fun initRepository(repo: Repository): Unit {
this.repository = repo
}
}
class Repository {
fun saveAmount(amount: Double) {}
}
在上面的代码中,我们在Presenter
类内部声明了一个可变的可为null的repository
属性,该属性为Repository
类型,然后在声明过程中将此属性初始化为null。 Presenter
类中有一个方法initRepository()
,稍后使用实际的Repository
实例重新初始化此属性。 请注意,也可以使用依赖注入器(例如Dagger)为该属性分配值。
现在,要在此repository
属性上调用方法或属性,我们必须执行空检查或使用安全调用运算符。 为什么? 因为repository
属性是可空类型( Repository?
)。 (如果您需要对Kotlin中的可空性进行复习,请访问Nullability,Loops和Conditions )。
// Inside Presenter class
fun save(amount: Double) {
repository?.saveAmount(amount)
}
为避免每次需要调用属性的方法时都必须执行null检查,我们可以使用lateinit
修饰符标记该属性-这意味着我们已将该属性(作为另一个类的实例)声明为延迟初始化 (即属性将在以后初始化)。
class Presenter {
private lateinit var repository: Repository
//...
}
现在,只要我们等到为属性赋值,就可以安全地访问属性的方法而无需执行任何空检查。 属性初始化可以通过setter方法或依赖注入进行。
repository.saveAmount(amount)
请注意,如果尝试在属性初始化之前访问属性的方法,则将获得kotlin.UninitializedPropertyAccessException
而不是NullPointerException
。 在这种情况下,异常消息将为“ lateinit属性存储库尚未初始化”。
还要注意在延迟使用lateinit
初始化属性时设置的以下限制:
- 它必须是可变的(用
var
声明)。 - 属性类型不能是原始类型,例如
Int
,Double
,Float
等等。 - 该属性不能具有自定义getter或setter。
2.内联属性
在“ 高级函数”中 ,我介绍了用于高阶函数的inline
修饰符-这有助于优化将lambda作为参数的任何高阶函数。
在Kotlin中,我们还可以在属性上使用此inline
修饰符。 使用此修饰符将优化对属性的访问。
让我们看一个实际的例子。
class Student {
val nickName: String
get() {
println("Nick name retrieved")
return "koloCoder"
}
}
fun main(args: Array<String>) {
val student = Student()
print(student.nickName)
}
在上面的代码中,我们有一个普通属性nickName
,它没有inline
修饰符。 如果我们使用Show Kotlin Bytecode功能反编译代码段(如果您在IntelliJ IDEA或Android Studio中,请使用Tools > Kotlin > Show Kotlin Bytecode ),我们将看到以下Java代码:
public final class Student {
@NotNull
public final String getNickName() {
String var1 = "Nick name retrieved";
System.out.println(var1);
return "koloCoder";
}
}
public final class InlineFunctionKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Student student = new Student();
String var2 = student.getNickName();
System.out.print(var2);
}
}
在上面生成的Java代码中(为了简洁起见,删除了部分代码),您可以看到在main()
方法内部,编译器创建了一个Student
对象,称为getNickName()
方法,然后打印其返回值。
现在让我们将属性指定为inline
,然后比较生成的字节码。
// ...
inline val nickName: String
// ...
我们只是在变量修饰符之前插入inline
修饰符: var
或val
。 这是为此内联属性生成的字节码:
// ...
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Student student = new Student();
String var3 = "Nick name retrieved";
System.out.println(var3);
String var2 = "koloCoder";
System.out.print(var2);
}
// ...
再次删除了一些代码,但要注意的关键是main()
方法。 吨他编译器复制属性get()
函数体,并将其粘贴到调用点(此机制类似于内联函数)。
我们的代码已经过优化,因为不需要创建对象并调用属性getter方法。 但是,如内联函数文章中所述,我们将拥有比以前更大的字节码-请谨慎使用。
还要注意,此机制适用于没有后备字段的属性(请记住,后备字段只是当您想要修改或使用该字段数据时属性使用的字段)。
3.扩展属性
在高级功能中,我还讨论了扩展功能-这些功能使我们能够使用新功能扩展类,而不必从该类继承。 Kotlin还为属性提供了一种类似的机制,称为扩展属性 。
val String.upperCaseFirstLetter: String
get() = this.substring(0, 1).toUpperCase().plus(this.substring(1))
在高级功能文章中,我们定义了一个接收器类型为String
的uppercaseFirstLetter()
扩展函数。 在这里,我们将其转换为顶级扩展属性。 请注意,您必须在属性上定义一个getter方法,此方法才能起作用。
因此,有了有关扩展属性的新知识,您就会知道,如果您曾经希望某个类应具有不可用的属性,则可以自由地创建该类的扩展属性。
4.数据类
让我们从一个典型的Java类或POJO(普通的旧Java对象)开始。
public class BlogPost {
private final String title;
private final URI url;
private final String description;
private final Date publishDate;
//.. constructor not included for brevity's sake
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BlogPost blogPost = (BlogPost) o;
if (title != null ? !title.equals(blogPost.title) : blogPost.title != null) return false;
if (url != null ? !url.equals(blogPost.url) : blogPost.url != null) return false;
if (description != null ? !description.equals(blogPost.description) : blogPost.description != null)
return false;
return publishDate != null ? publishDate.equals(blogPost.publishDate) : blogPost.publishDate == null;
}
@Override
public int hashCode() {
int result = title != null ? title.hashCode() : 0;
result = 31 * result + (url != null ? url.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (publishDate != null ? publishDate.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "BlogPost{" +
"title='" + title + '\'' +
", url=" + url +
", description='" + description + '\'' +
", publishDate=" + publishDate +
'}';
}
//.. setters and getters also ignored for brevity's sake
}
如您所见,我们需要显式地编写类属性访问器的代码:getter和setter以及hashcode
, equals
和toString
方法(尽管IntelliJ IDEA,Android Studio或AutoValue库可以帮助我们生成它们)。 我们通常在典型的Java项目的数据层中看到这种样板代码。 (为简便起见,我删除了字段访问器和构造函数)。
很棒的事情是,Kotlin团队为我们提供了类的data
修饰符,以消除编写这些样板的麻烦。
现在让我们用Kotlin编写前面的代码。
data class BlogPost(var title: String, var url: URI, var description: String, var publishDate: Date)
太棒了! 我们只需要在class
关键字之前指定data
修饰符即可创建数据类,就像我们在上述BlogPost
Kotlin类中所做的一样。 现在将为我们创建equals
, hashcode
, toString
, copy
和多个组件方法。 请注意,数据类可以扩展其他类(这是Kotlin 1.1的新功能)。
equals
方法
此方法比较两个对象是否相等,如果相等则返回true,否则返回false。 换句话说,它比较两个类实例是否包含相同的数据。
student.equals(student3)
// using the == in Kotlin
student == student3 // same as using equals()
在Kotlin中,使用等于运算符==
将在后台调用equals
方法。
hashCode
方法
此方法返回一个整数值,该值用于快速存储和检索存储在基于哈希的集合数据结构中的数据,例如HashMap
和HashSet
集合类型中的数据。
toString
方法
此方法返回对象的String
表示形式。
data class Person(var firstName: String, var lastName: String)
val person = Person("Chike", "Mgbemena")
println(person) // prints "Person(firstName=Chike, lastName=Mgbemena)"
通过仅调用类实例,我们得到了一个返回给我们的字符串对象-Kotlin为我们在幕后将对象称为toString()
。 但是,如果我们不输入data
关键字,请查看我们的对象字符串表示形式是什么:
com.chike.kotlin.classes.Person@2f0e140b
信息少得多!
copy
方法
此方法使我们可以创建具有所有相同属性值的对象的新实例。 换句话说,它创建对象的副本。
val person1 = Person("Chike", "Mgbemena")
println(person1) // Person(firstName=Chike, lastName=Mgbemena)
val person2 = person1.copy()
println(person2) // Person(firstName=Chike, lastName=Mgbemena)
关于Kotlin中的copy
方法,一个很酷的事情是在复制过程中更改属性的能力。
val person3 = person1.copy(lastName = "Onu")
println(person3) //Person3(firstName=Chike, lastName=Onu)
如果您是Java程序员,则此方法类似于您已经熟悉的clone()
方法。 但是Kotlin copy
方法具有更强大的功能。
破坏性声明
在Person
类中,由于在类中放置了data
关键字,因此编译器还为我们自动生成了两个方法。 这两种方法都以“ component”为前缀,然后加上数字后缀: component1()
, component2()
。 这些方法中的每一个都代表类型的各个属性。 请注意,后缀对应于在主构造函数中声明的属性的顺序。
因此,在我们的示例中,调用component1()
将返回名字,而调用component2()
将返回姓氏。
println(person3.component1()) // Chike
println(person3.component2()) // Onu
但是,使用这种样式调用属性很难理解和阅读,因此显式调用属性名称要好得多。 但是,这些隐式创建的属性确实具有非常有用的目的:它们使我们可以进行解构声明,在声明中可以将每个组件分配给局部变量。
val (firstName, lastName) = Person("Angelina", "Jolie")
println(firstName + " " + lastName) // Angelina Jolie
我们在这里所做的是直接将Person
类型的第一个和第二个属性( firstName
和lastName
)分别分配给变量firstName
和lastName
。 我还在Packages and Basic Functions帖子的最后部分中讨论了这种称为解构声明的机制。
5.嵌套类
在“ 函数的更多乐趣”一文中 ,我告诉您Kotlin支持局部或嵌套函数,该函数在另一个函数中声明。 好吧,Kotlin同样也支持嵌套类-在另一个类内部创建的类。
class OuterClass {
class NestedClass {
fun nestedClassFunc() { }
}
}
我们甚至可以调用嵌套类的公共函数,如下所示-Kotlin中的嵌套类等效于Java中的static
嵌套类。 请注意,嵌套类不能存储对其外部类的引用。
val nestedClass = OuterClass.NestedClass()
nestedClass.nestedClassFunc()
我们还可以自由设置嵌套类的私有这意味着我们只能创建一个实例NestedClass
的范围内, OuterClass
。
内部阶层
另一方面,内部类可以引用在其中声明的外部类。要创建内部类,我们在嵌套类中将inner
关键字放在class
关键字之前。
class OuterClass() {
val oCPropt: String = "Yo"
inner class InnerClass {
fun innerClassFunc() {
val outerClass = this@OuterClass
print(outerClass.oCPropt) // prints "Yo"
}
}
}
在这里,我们引用OuterClass
从InnerClass
通过使用this@OuterClass
。
6.枚举类
枚举类型声明一组由标识符表示的常量。 这种特殊的类是由在class
关键字之前指定的关键字enum
创建的。
enum class Country {
NIGERIA,
GHANA,
CANADA
}
要根据其名称检索枚举值(就像在Java中一样),我们可以这样做:
Country.valueOf("NIGERIA")
或者我们可以使用Kotlin enumValueOf<T>()
帮助程序方法以通用方式访问常量:
enumValueOf<Country>("NIGERIA")
同样,我们可以像这样获取所有值(例如对于Java枚举):
Country.values()
最后,我们可以使用Kotlin enumValues<T>()
帮助程序方法以通用方式获取所有枚举条目:
enumValues<Country>()
这将返回一个包含枚举条目的数组。
枚举构造函数
就像普通的类一样, enum
类型可以具有自己的构造函数,该构造函数具有与每个枚举常量关联的属性。
enum class Country(val callingCode: Int) {
NIGERIA (234),
USA (1),
GHANA (233)
}
在Country
枚举类型的主要构造函数中,我们为每个枚举常量定义了不可变的属性callingCodes
。 在每个常量中,我们将一个参数传递给构造函数。
然后,我们可以像下面这样访问constants属性:
val country = Country.NIGERIA
print(country.callingCode) // 234
7.密封班
Kotlin中的密封类是抽象类(您永远都不想用它创建对象),其他类可以扩展该抽象类。 这些子类在密封的类体内定义在同一文件中。 因为所有这些子类都在密封的类体内定义,所以我们只需查看文件就可以知道所有可能的子类。
让我们看一个实际的例子。
// shape.kt
sealed class Shape
class Circle : Shape()
class Triangle : Shape()
class Rectangle: Shape()
要声明一个类是密封的,我们插入sealed
的前修改class
的类声明修饰符头,在我们的例子中,我们宣布Shape
类作为sealed
。 没有其子类的密封类是不完整的,就像典型的抽象类一样,因此我们必须在同一文件(在本例中为shape.kt)中声明各个子类。 请注意,您不能从另一个文件定义密封类的子类。
在上面的代码中,我们指定Shape
类只能由Circle
, Triangle
和Rectangle
类扩展。
Kotlin中的密封类具有以下附加规则:
- 我们可以将修饰符
abstract
添加到密封类中,但这是多余的,因为缺省情况下密封类是抽象的。 - 密封的类不能具有
open
或final
修饰符。 - 我们还可以自由地将数据类和对象声明为密封类的子类(它们仍需要在同一文件中声明)。
- 密封的类不允许具有公共构造函数-默认情况下,其构造函数是私有的。
扩展密封类的子类的类可以放在同一文件中,也可以放在另一个文件中。 密封类的子类必须用open
修饰符标记(您将在下一篇文章中了解有关Kotlin继承的更多信息)。
// employee.kt
sealed class Employee
open class Artist : Employee()
// musician.kt
class Musician : Artist()
密封类及其子类在when
表达式中非常方便。 例如:
fun whatIsIt(shape: Shape) = when (shape) {
is Circle -> println("A circle")
is Triangle -> println("A triangle")
is Rectangle -> println("A rectangle")
}
在这里,编译器是聪明的,以确保我们覆盖了所有可能的when
的情况。 这意味着无需添加else
子句。
如果要改为执行以下操作:
fun whatIsIt(shape: Shape) = when (shape) {
is Circle -> println("A circle")
is Triangle -> println("A triangle")
}
该代码无法编译,因为我们没有包括所有可能的情况。 我们将出现以下错误:
Kotlin: 'when' expression must be exhaustive, add necessary 'is Rectangle' branch or 'else' branch instead.
因此,我们可以包含is Rectangle
大小写,也可以包含else
子句来完成when
表达式。
结论
在本教程中,您了解了有关Kotlin中的类的更多信息。 我们介绍了有关类属性的以下内容:
此外,您还了解了一些很酷的高级类,例如数据,枚举,嵌套和密封类。 在Kotlin From Scratch系列的下一篇教程中,将向您介绍Kotlin中的接口和继承。 再见!
翻译自: https://code.tutsplus.com/tutorials/kotlin-from-scratch-advanced-properties-and-classes--cms-29613