Overview
类在面向对象编程中是一个最基本的概念。类是对象的模板,用于产生具有相同结构的对象。一个类通常由属性和行为构成,属性体现在成员变量上,行为体现在方法上。
Java 篇
创建一个类
在 Java 中使用关键字 class
来定义一个类
例:
public class Person {
// 属性
private int age;
// 行为
public void say(String message) {
System.out.println(message);
}
}
创建一个对象
创建一个类的实例需要使用关键字 new
,大部分语言中都是同样的做法。
语法:类名 对象名 = new 类名(构造函数参数);
例:
Person person = new Person();
Setter 与 Getter
在 Java 中,通常由于保护类的封装性以及 Java Bean 的规约,所有属性都会被声明为私有属性,然后提供公有的 Setter 和 Getter 方法来修改和访问属性,对于只读属性则只提供 Getter 方法。
例:
比如说有一个 age
属性,通常都会声明为 private int age
,然后再定义以下 Setter 和 Getter 方法
常见的 Setter 方法
public void setAge(final int age) {
this.age = age;
}
常见的 Getter 方法
public int getAge() {
return age;
}
构造方法
构造方法在类中用于构造对象,提供一些在对象创建时就应该被赋予的属性。在 Java 中,默认每个类都有一个无参的构造方法,但是如果你已经在类中定义了构造方法,那么默认的无参构造方法就不会被创建。
当一个类包含多个属性时,最常见的做法是提供了一个可以为所有属性赋值的构造方法,然后重载该构造方法,并且在这些构造方法中调用拥有所有公开属性的构造方法。
public class Man {
private String name;
private int age;
private final String from = "USA";
private String description = "none";
public Man() {
}
// 赋值所有属性
public Man(String name, int age, String description) {
this.age = age;
this.description = description;
this.name = name;
}
public Man(String name, int age) {
this(name, age, null);
}
public Man(int age) {
this(null, age);
}
public Man(String name) {
this(name, 0);
}
}
基于构造方法构建实例
Man fred = new Man("Fred", 21);
Man peter = new Man("Peter");
Man jack = new Man(21);
可以看到,如果一个类属性很多并且在构造时需要使用不同途径,那么重载构造方法会是一种非常繁琐且无聊的工作,这要是 Java 长久以来的一个弊端。
不可变对象
不可变对象在并发编程中占有非常重要的地位。但是 Java 中并没有在语法层面提供用于产生不可变对象的类,你必须自己手动定义。最简单的实现方法就是将类的所有属性声明为 private final
并且在构造方法中构造所有属性,此外也不提供任何 Setter 方法,而 Getter 方法z则只返回基本类型数据或者引用类型数据的不可变形态或副本。
public class ImmutableSong {
private final String name;
private final String artist;
private final Date publishDate;
public ImmutableSong(final String name,
final String artist,
final Date publishDate) {
this.artist = artist;
this.name = name;
this.publishDate = publishDate;
}
public String getArtist() {
return artist;
}
public String getName() {
return name;
}
public Date getPublishDate() {
return new Date(publishDate.getTime());
}
}
Groovy 篇
创建一个类
Groovy 中也使用关键字 class
来定义一个类
例:
class Person {
// 属性
def age
// 行为
def say(message) {
println(message)
}
}
创建一个对象
创建一个类的实例需要使用关键字 new
,大部分语言中都是同样的做法。
创建对象时,参照 变量与常量
一节,可以使用动态类型也可以使用静态类型
例:
def person = new Person()
Person person = new Person()
Setter 与 Getter
与 Java 不同, 当你在类中定义了属性时 Groovy 会自动提供对应的 Setter 与 Getter 方法,并且当你直接使用属性时其实就是调用这些方法。
例:
def person = new Person()
// 调用属性修改 age
person.age = 10
assert person.age == 10
// 调用方法修改 age
person.setAge(12)
assert person.age == 12
可以看到为 Person
类定义了属性 age
后,无论直接调用属性还是调用方法都改变了 age
的值。
实际上以上的 Person
会被翻译成如下代码
public class Person implements GroovyObject {
private Object age;
public Person() {
CallSite[] var1 = $getCallSiteArray();
MetaClass var2 = this.$getStaticMetaClass();
this.metaClass = var2;
}
public Object getAge() {
return this.age;
}
public void setAge(Object var1) {
this.age = var1;
}
}
当然你也可以像 Java 一样在类中自定义 Setter 和 Getter 方法
例:
class Person {
private def privateAge
// 自定义 privateAge 的 Setter 和 Getter 方法
def getPrivateAge() {
return privateAge
}
def setPrivateAge(n) {
if (n > privateAge) {
privateAge = n
}
}
}
调用自定义的 Setter 和 Getter
person.privateAge = 20
person.privateAge = 14
assert person.age == 20
当我们再次修改 privateAge
的值为 14
时,由于自定义的 Setter 方法不允许年龄被减少,所以这次修改并没成功。这一例子也证明了访问属性其实是调用了方法。
Groovy 中当你定义了一个没有访问控制符的属性时,语言会自动提供 Setter 和 Getter 方法,此时这种属性在 Groovy 中被称作为
Properties
。而如果属性本身就声明为 public 的话,则不会提供 Setter 和 Getter,这种属性被称作Fields
。本系列中都统一称作为属性,因为Public Fields
本身就并不建议使用。
构造方法
Groovy 中无需像 Java 那样手动创建一个个构造方法,Groovy 会自动为我们生成。
class Man {
def name
def age
private def from = "USA"
def description = "none"
}
在创建时可以通过 propertyName: propertyValue
指定使用哪个属性来构造实例
def fred = new Man(name: "Fred", age: 21)
def peter = new Man(name: "Peter")
def jack = new Man(age: 21)
def terry = new Man(from: "LA", description: "A man from LA.")
以上四个实例都可以正常使用,如果细心的话,你会发现 from
被声明为 private
,虽然无法通过对象进行修改,但是通过构造方法修改它的值却是可能的,很不幸这也是 Groovy 的一个历史遗留 Bug。
不可变对象
Groovy 中可以为一个类添加注解 @Immutable
来表明该类的所有属性都不可变。一旦一个类声明为 @Immutable
则此类的所有属性必须明确指定类型而不能使用 def
来定义。
@Immutable
class ImmutableSong {
String name
String artist
Date publishDate
}
Scala 篇
创建一个类
Scala 中也使用关键字 class
来定义类,但是类的属性必须明确指明初始值,而不是像 Java 和 Groovy 一样有缺省的默认值。
class Person {
// 属性
var age = 0
// 行为
def say(message: String) {
println(message)
}
}
创建一个对象
创建一个类的实例需要使用关键字 new
,如果调用无参构造方法时可以省略代表构造方法的小括号
val person = new Person()
// 省略小括号
val person = new Person
Setter 与 Getter
与 Groovy 一样,Scala 也提供了默认的 Setter 和 Getter 方法,当你直接使用属性时其实就是调用这些方法。但是和 Groovy 不一样,Scala 中这两个方法的名字比较特别。Scala 中 默认的 Setter 方法名为 属性名_=
,Getter 方法名为 属性名
。
例:
val person = new Person
// 修改 age,实际调用的是 person.age_=(10),即 setter 方法为 age_=()
person.age = 10
// 读取 age,实际调用的是 person.age(),即 getter 方法为 age()
println(person.age)
当然你也可以在类中按照以下规则自定义 Setter 和 Getter 方法。
Scala 中自定义 Setter 和 Getter 的步骤如下:
- 声明属性为
private
- 提供修改该属性的 Setter 方法
_=
- 提供访问该属性的 Getter 方法
例:
class Person {
private var privateAge = 0
def trueAge = privateAge
def trueAge_=(pAge: Int) {
if (pAge > privateAge) {
privateAge = pAge
}
}
}
调用自定义的 Setter 和 Getter
person.trueAge = 20
person.trueAge = 14
println(person.trueAge) //20
如果一个属性被声明为值 (val) 的话,则 Scala 只会为其提供默认的 Getter 方法,而没有 Setter 方法。
对象私有域
对象私有域是一个 Scala 比较特别的地方,它可以控制一个属性只能在该对象内部调用。这一概念可能理解起来比较困难,我们先看一个一般的例子
class Person {
var age = 0
def isYounger(other: Person) = age < other.age
}
在以上代码中,isYounger()
方法接收类的另一个实例,并且将这个实例的 age
属性与当前实例的 age
属性进行比较。如果使用对象私有域修饰 age
的话则isYounger()
虽然可以接收另一个对象,但无法访问传入对象的 age
属性,只能访问当前对象自身 age
属性。
这种对象私有域在 Scala 中使用 private[this]
这种语法来声明
例:
class Person {
// 对象私有域
private[this] var age = 0
// 此时以下调用方式就会报错
// def isYounger(other: Person) = age < other.age
}
构造方法
Scala 中构造方法分为主构造方法和副构造方法。
主构造方法
主构造方法紧跟在类的声明之后
class Man(val name: String, var age: Int){}
主构造方法的参数可以声明为 val
或 var
,使用方法与其声明为成员变量时相同。
此外,Scala 中在类内部声明的所有可执行语句都属于主构造器,在对象被创建时都会被调用。
例:
class Man(val name: String, var age: Int){
println("Sentences in Main Constructor")
}
每创建一个 Man
的实例,语句 "Sentences in Main Constructor" 都会被打印。
副构造方法
副构造方法使用 this()
声明。所有副构造方法必须在第一行调用主构造方法或其它副构造方法。
例:
// 主构造方法
class Man(val name: String, var age: Int) {
// 副构造方法
def this(name: String) {
// 调用主构造方法
this(name, 0)
}
def this(age: Int) {
// 调用主构造方法
this("Default Name", age)
}
def this() {
// 调用其它副构造方法
this("Default Name")
}
}
创建实例时可以使用主构造方法也可以使用副构造方法
val fred = new Man("Fred", 21) // Main Constructor
val peter = new Man("Fred") // Slave Constructor
val jack = new Man(21) // Slave Constructor
私有主构造方法
我们可以为主构造方法添加关键字 private
从而声明主构造方法为私有权限,此时类只能通过副构造方法进行创建。
例:
class Woman private(val name: String, val age: Int) {
def this(name: String) {
this(name, 0)
}
}
// Wrong!! 无法调用私有主构造方法
// val mary = new Woman("Mary",21)
val jane = new Woman("Jane")
深入构造方法
在使用构造方法时我们也可以为每个参数指定默认值
例:
class Man(val name: String = "Default Name",
var age: Int = 0){}
在声明构造方法的参数时我们也可以像在类中添加成员一样添加访问控制符
例:
class Man(val name: String,
var age: Int,
private var from: String = "USA") {}
此外我们也可以不使用 var
或 val
修饰构造方法中的变量,此时就相当于定义了一个对象私有域
例:
class Man(val name: String,
var age: Int,
description: String = "none") {}
Java 风格的 Bean
因为 Scala 的 Setter 和 Getter 方法名并不符合 Java Bean 的规约,所以在使用一些 Java 的利用到反射性质的类库时可能会发生问题。此时可以选择让 Scala 生成 Java 风格的 Setter 与 Getter。
使用方式很简单,只需在 Boolean 型属性上添加注解 BooleanBeanProperty
,在其它属性上添加注解 BeanProperty
即可。
例:
class Model {
@BeanProperty
var name = ""
@BooleanBeanProperty
var visible = false
}
不可变对象
Scala 并没有直接提供这样一种类(尽管样本类之类的看起来有那么点像)。但是由于 Scala 本身提供了大量不可变类作为默认实现,且 Scala 官方建议总是使用 val
声明变量,所以你只要遵守这些规定那么编写不可变对象也不是件难事。
Kotlin 篇
定义一个类
Kotlin 同 Scala 一样使用关键字 class
来定义类,类的属性必须明确指明初始值。
例:
class Person {
// 属性
var age = 0
// 行为
fun say(message: String) {
println(message)
}
}
创建一个对象
创建一个类的实例不用像其它语言一样使用 new
,直接添加括号就行
例:
val person = Person()
Kotlin 文件中可以定义多个类。
Setter 与 Getter
与 Scala 一样提供了默认 Setter 和 Getter 方法,且默认的 Setter 方法名为 set()
,Getter 方法名为 get()
。当你直接使用属性时其实就是调用这些方法。
例:
val person = Person()
// 修改 age
person.age = 10
// 读取 age
println(person.age)
Kotlin 中自定义 Setter 和 Getter 与其它语言都不一样,需要紧跟着属性名声明 set()
与 get()
例
class Person {
var trueAge: Int
get() = age
set(pAge) {
if (pAge > age) {
age = pAge
}
}
}
调用自定义的 Setter 和 Getter
例:
person.trueAge = 20
person.trueAge = 14
println(person.trueAge) //20
如果一个属性被声明为值的话,则 Kotlin 只会为其提供默认的 Getter 方法,而没有 Setter 方法。
例:
val birthday = Date()
你也可以仅仅自定义 Setter 和 Getter 的访问权限而不是实现
例:
var setterVisiblity: String = "foo"
private set
Backing 域
由于无法直接访问属性,调用属性实质就是调用方法,所以如果上节改写为如下形式的话会陷入自己调用自己的无限循环中
例:
var age: Int
get() = age
set(pAge) {
if (pAge > age) {
age = pAge
}
}
为此,Kotlin 提供了 Backing 域来直接访问,以前版本的 Backing 域是以 $
开头的特殊字符串,但是从 1.0
开始 Backing 域就是关键字 field
。
例:
var backAge: Int = 0
set(pAge) {
if (pAge > field) {
field = pAge
}
}
构造方法
Kotlin 中构造方法也分为主构造方法和副构造方法,只不过 Scala 中称为 "Main Constructor" 和 "Slave Constructor",而 Kotlin 中称为 "Primary Constructor" 和 "Secondary Constructor"。
主构造方法
主构造方法紧跟在类的声明之后
class Man(val name: String, var age: Int){}
主构造方法的参数可以声明为 val
或 var
,使用方法与其声明为成员变量时相同。
此外,类中可以声明 init
语句块,该语句块中的所有可执行语句都属于主构造器,在对象被创建时都会被调用。 所以以上类可以改写为以下形式:
class Man(val name: String, var age: Int){
init {
println("Sentences in primary constructor")
}
}
每创建一个 Man
的实例,语句 "Sentences in Main Constructor" 都会被打印。
副构造方法
副构造方法使用 constructor()
声明。所有副构造方法都必须调用主构造方法或其它副构造方法。
例:
// 主构造方法
class Man(val name: String, var age: Int) {
// 副构造方法
// 调用主构造方法
constructor(name: String) : this(name, 0) {
}
// 调用主构造方法
constructor(age: Int) : this("Default Name", age) {
}
// 调用其它副构造方法
constructor() : this("Default Name") {
}
}
创建实例时可以使用主构造方法也可以使用副构造方法
例:
val fred = Man("Fred", 21) // Primary Constructor
val peter = Man("Fred") // Secondary Constructor
val jack = Man(21) // Secondary Constructor
私有主构造方法
我们可以为主构造方法添加关键字 private constructor
从而声明主构造方法为私有权限,此时类只能通过副构造方法进行创建。
例:
class Woman private constructor(val name: String, val age: Int) {
constructor(name: String) : this(name, 0) {
}
}
// Wrong!! 无法调用私有主构造方法
// val mary = Woman("Mary",21)
val jane = Woman("Jane")
深入构造方法
在使用构造方法时我们也可以为每个参数指定默认值
例:
class Man(val name: String = "Default Name",
var age: Int = 0){}
在声明构造方法的参数时我们也可以像在类中添加成员一样添加限定符
例:
class Man(val name: String,
var age: Int,
private var from: String = "USA") {}
Empty 类
Kotlin 中允许声明一个没有类体的类用于仅仅表示类型
例:
class Empty
不可变对象
Kotlin 中有一些方法可以实现部分功能,但是语言本身也没有直接提供这样一种类。
Summary
- Scala 和 Kotlin 都有主副构造方法
- Scala 和 Kotlin 都有独自的 Setter 和 Getter 方法
- 除了 Java 外,另三种语言访问属性就是访问对应的方法
文章源码见 https://github.com/SidneyXu/JGSK 仓库的 _15_class
小节