Java & Groovy & Scala & Kotlin - 15.类

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){}

主构造方法的参数可以声明为 valvar ,使用方法与其声明为成员变量时相同。

此外,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") {}

此外我们也可以不使用 varval 修饰构造方法中的变量,此时就相当于定义了一个对象私有域

例:

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){}

主构造方法的参数可以声明为 valvar ,使用方法与其声明为成员变量时相同。

此外,类中可以声明 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 小节

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,173评论 9 118
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 1.项目经验 2.基础问题 3.指南认识 4.解决思路 ios开发三大块: 1.Oc基础 2.CocoaTouch...
    阳光的大男孩儿阅读 4,971评论 0 13
  • 目录 《真实的幸福》-精读 Day 1 №1 幸福的优势 №2 积极心理学中的幸福 №3 评估你的幸福 №4 习得...
    巴山小羊儿阅读 1,251评论 0 3
  • 总是在第二天上课有任务的时候 睡眠才会特别好 在家做了一天的梦 收到了公寓的警告 说我的狗狗有噪音 被邻居举报 ...
    踪迹阿阅读 272评论 0 0