本教程出自于白师傅、本节关键词: 可访问性控制、修饰符, 代词this, 构造函数

可访问性控制

关注 HelloJava 类的第 9 行, 这里我们直接通过 p.name = "小花" 对对象 p 的内部属性进行了操作, 这事实上破坏了对象的”封装性”, 这是不可取的!

也就是说, 我们应该尽可能地将对象的成员 ( 属性/方法 ) 封装在内部, 而不是随意地向外界暴露, 这才更符合封装的原则. 这就像你的钱总得藏着点, 不能随意让外人使用.

请自行查阅关于 “开/闭原则” 的资料.

那么怎么把不想向外界暴露的成员藏起来呢? 这就要说到一个新的话题: 可访问性控制

某些材料上也叫 “可视性/可见性”

打开 Pig 类的源代码, 将 name 属性前的 public 修饰符改为 private, 代码如下:

private String name;

保存 ( Ctrl + S ) ……

之后我们注意到在 HelloJava 类的图标上出现了一个红色的叉, 这就说明出问题了……

切换到 HellowJava 类的代码视图 (如下图), 把鼠标放在划红线的那里, 看看人家说什么了……

大意是: Pig 类里的 name 属性 (filed, 字段) 不可见! 也就是说, 在 HelloJava 的 main 方法中不能直接访问 name 属性了.

看出 private 修饰符的作用了吧~ 它把 name 属性变成私有的了, 藏起来了……


在定义, 属性方法时, 我们可以添加访问控制修饰符, 以控制其可访问性.

Java中有 3 种访问控制修饰符, 它们的作用如下:

private : 私有的, 只在当前类中可访问.

protected : 被保护的, 只在当前类, 当前类的子孙类 (子孙后代), 以及与当前类同属一个包 (package)的其它类中可访问.

public : 公有的, 在所有类中均可访问.

那么…… 如果不写任何修饰符时”可访问性”又是如何呢? 这种情形下通常称作 default 修饰符, 它并不等同private, protectedpublic中的任何一个!

具体的差异可参看下表:

修饰符 当前类 同一个包里的类 子孙类 其他包里的类
public YES YES YES YES
protected YES YES YES NO
default YES YES NO NO
private YES NO NO NO

现在我们成功地把 name 属性私有化了, 那么如果有需要时, 外界怎么对它进行操作呢?

Pig类中添加代码, 让它变成下面的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.bailey.study.animal;
public class Pig {
private String name;
public String sex;
public void eat(String food) {
System.out.println(name + "吃了" + food);
}

public void setName(String name) {
this.name = name; // 在任何对象中, 代词 this 均指代当前对象
}

public String getName() {
return name;
}
}

我们为 Pig 类添加了2个方法: setNamegetName.

这两个方法使用 public 修饰, 这样外界就可以通过这两个方法对 name 属性进行赋值或取值了. ( 相应地, 应该把HelloJava类中的 p.name = "小花" 改成 p.setName("小花"); )

这样做的好处是: 我们可以灵活控制外界对 name 属性的访问.

例如: 当需要在给 name 属性赋值时做一些额外的附加的操作 (如: 数据合法性校验), 就可以把代码写在 setName 方法中. 当然, 如果不想让外界取得name的值, 那把getName方法去掉就行了.

对于 setXXX, getXXX 这样的方法, 通常称为 settergetter方法.

上面代码中第 10 行很有趣: this.name = name;

其中, this.name 指的是当前对象的name属性 (在对象 A 中, this.name就是 A 的name属性, 而在对象 B 中, 它就是 B 的name属性). 而 “=” 后面的 name 指的是setName方法的name参数 (形参).

这里的代词 this 指向当前对象. 类似地, 还有一个代词: super, 它指向当前类的直接父类.

关于 this 还有另外一层意思, 请继续往下看, 在讲述”构造函数”时将会提及.

构造函数

接下来我们插播一个内容: 构造函数.

所谓”构造函数”其实是一种特别的函数, 它的名称与类名相同, 在实例化的时候构造函数将被首先执行.

通常在构造函数中做一些初始化的工作.

现在在Pig类中添加一个方法, 代码如下:

1
2
3
4
5
public Pig(String name, String sex) {
this.name = name;
this.sex = sex;
System.out.println(name + "出生啦(Constructed)!");
}

这样我们为Pig类添加了一个构造函数. 相应地, 在实例化时就应该传入所需的参数, 例如: Pig p = new Pig("小花", "雄"); 赶快试试吧~

与普通方法一样, 构造函数也可以被重载.

试着再为Pig类添加一个构造函数, 代码如下:

1
2
3
public Pig(String name) {
this(name, "未知"); // 调用上面定义那个构造函数
}

注意一下第 2 行, 又见到 this 了. 在这里 this 特指当前类的构造函数, 事实上第 2 行的写法就是告诉 Java, 根据传入的参数去选择调用一个合适的构造函数.

HelloJava.java 中的 Pig p = new Pig("小花", "雄"); 改成 Pig p = new Pig("小花"); 设置断点, 跟踪调试一下吧~

关于构造函数, 还有几点需要注意:

(1) 当一个类中没有明确定义任何构造函数时会存在一个不带任何参数 ( 形参表为空 ) 的构造函数

类似这样: public Pig() { }

这个构造函数称作默认构造函数. 而当我们明确地定义了任何一个或多个构造函数后, 上述默认构造函数就消失了.

(2) 若父类中无默认构造函数 ( 形参表为空的构造函数, 如: Pig() { … } ), 则子类中必须至少明确定义一个构造函数. 并使用super(...)调用父类构造函数. (若看不懂, 暂时放一下, 在学习到继承时就明白了)

(3) 若构造函数中出现了 this(...); 那么必须把它写在当前构造函数中的第 1 行.


插播完”构造函数”的介绍后, 让我们再回到修饰符的介绍…

前面提到的 public, protected, private 修饰符主要是对可访问性 (可视性) 进行控制, 而现在将要讨论的这两个修饰符 ( finalstatic ) 并不影响可访问性.

具体是怎样的? 现在就来看看吧…

final

添加了 final 修饰符的属性、方法、类将被认为是”最终版”

  • 若修饰属性, 表示该属性在初始赋值后将不可重新赋值 (类似”常量”)
  • 若修饰方法, 表示该方法是不能被覆盖
  • 若修饰, 则表示该类不能被继承

final 修饰方法 和 置于类声明之前的情形, 涉及到 “继承”, 因到此为止尚未谈及如果实现继承, 暂不举例. 可先记住前述的规则.

先看修饰属性的情形:

修改一下上节例子中的 Pig 类, 为 sex 属性加上 final 修饰符 (我认为性别一旦设定就不能乱变了, 呵呵~).

1
final public String sex;

OK, 现在试试在别的地方给sex赋值一下…

是不是Eclipse已经提示错误了~

也许, 你已经想到了一个问题, 既然刚才我们说final用于修饰属性时类似”常量”, 那么应该象 C 语言一样, 在声明的时候就必须赋予初始值呀! 显然, 上面那句代码是没有给 sex 属性赋初值的, 那怎么 Eclipse 在这个时候为什么又不报错了呢?

呵呵, 秘密就在我们上节定义的构造函数中~

我们在构造函数中通过代码 this.sex = sex; 对其进行了初始化. ( 记住: 构造函数在对象实例化的第一时间被执行 )

小结一下, final修饰属性时只能在 3 个地方对该属性赋值(三者取其一):

(1) 声明同时, 例如: final public String sex = “雄”;

(2) 在构造函数中. 并且, 即使是在构造函数中也只能赋值一次.

(3) 在静态代码块中赋值 ( 什么是静态代码块? 简单来说, 就是使用 static 关键词修饰的一块代码, 类似 static { … }, 后文介绍 )

总之…… final修饰的属性, 只能被赋值一次, 之后…… 该属性就是”终态”的了……

final 还可以放置在方法的参数前 或 方法体中的声明的变量之前, 此时的含意亦与”常量”类似.

例如:

1
2
3
4
5
public void f (final String param) {
final int x = 3;
param = "Another"; // 不允许
x = 5; // 不允许
}

static

static 意为”静态”的, 相信学过 C 语言的同学对它并不陌生.

这里所谓的 “静态”, 可姑且将其理解为:

(1) 被 static 修饰的东东从属于类, 而非实例 (对象), 因此, 与类同生共灭

(2) 被 static 修饰的属性将被该类的所有实例 (对象) 共享

呵呵, 很不好理解吧~

在解释之前我们先定义几个术语, 以便描述:

(1) 实例变量 : 没有 static 修饰的属性. ( 此前例子中的 name, sex 都是实例变量 )

(2) 静态变量 : 有 static 修饰的属性

(3) 局部变量 : 定义在方法 (函数) 体中的变量

(4) 静态方法 : 有 static 修饰的方法 (函数)

OK, 继续…

  • 实例变量 事实上是属于具体的实例 (对象) 的, 只有使用 new 关键词将类实例化为对象后, 它们才实际存在, 所以我们说它们是从属于具体实例 (对象) 的, 所以叫它们 “实例变量”.

    我们在定义类的时候, 只是声称属于该类的所有实例 (对象) 均必须有这些属性 (实例变量), 而那个时候 ( 定义类的时候) 还没有任何实例产生, 因此, 这些实例变量也并未真正分配存储空间.

  • 静态变量 则是属于的, 它被属于该类的所以实例 (对象) 所共享.

    只要存在, 此属性就已经存在了, 而不必等到产生具体的实例 (对象) 才产生此属性. “类” 相对于 “对象” 而言是 “静的” , 所以这样的属性称作 “静态变量”.

​ 看下图可能更好理解:

哎呀~ 画图的水平真是不行, 还是辅以文字说明一下吧……

上图中, x实例变量, 当我们通过 new Test() 创建出 3 个实例后 ( 实例A、实例B、实例C ) , 即会产生x的 3 个独立副本, 可以通过 a.x = 1; b.x = 2; c.x = 3; 分别赋值.

若继续执行 a.x = 4, 只会导致实例 A 的 x 值变为 4, 而不会影响到实例 B 和 C .

然而, y 为静态变量, 被实例 A、B、C 所共享, 即: a.y, b.y, c.y 是同一个东东, 值都是 5, 若执行 a.y = 6, 则 a.y, b.y, c.y 都将等于 6.

这就是实例变量静态变量的区别. 所以, 我们可以说 “静态变量” 并不属于具体的某个实例, 而是属于类.

因此, 在写程序的时候, 我们通常不像上面写 a.y = 6, 而是写成 Test.y = 6 ( 使用类名引用, 而非对象 )

static 被用于修饰某个方法(函数)时, 该方法就被称作该类的静态方法, 它同样不依附于任何实例. 所以, 即使没有任何实例存在, 也可以使用类似 Test.f(); 的方式来调用静态方法 f()

对于 static 还有一个特别的用法: 用来修饰一段代码, 姑且称其为”静态代码块“吧, 例如:

1
2
3
4
5
6
7
public class Test {
public static int z = 0;

static {
System.out.println("static 修饰的代码被执行了...");
}
}

其中, static { .... }大括号内的代码将在首次使用Test类的时候被自动执行……

你可以在 HelloJava.java 中写上一句 System.out.println(Test.z); 运行程序试试~ 是不是输出下面的结果了:

1
2
static 修饰的代码被执行了...
0

说明一下: 上面第 1 行是 “静态代码块” 里输出的, 而第 2 行是新写入的 System.out.println(Test.z); 输出的.

呵呵, 有意思吧……

小结:

(1) static 可以修饰 属性 (静态变量), 方法 (静态方法), 或一段代码.

(2) static 修饰的东西是从属于类的, 生命周期与相同, 不依赖于实例.

(3) 正因第 (2) 条所述 ( 从属于类, 不依赖于实例 ), 所以… 有可能静态方法静态代码块被调用/执行的时候还没有任何实例存在. 因此, 不能在静态方法或静态代码块中访问/调用非静态成员 ( 如: 实例变量, 非静态方法 )

static也可以放在内部类的定义前, 这样的类称为静态内部类. 关于什么是内部类, 什么是静态内部类, 它们和普通的类有什么区别, 我们这里暂不展开, 只是提一下 static 还可以使用在类定义时.    

好了, 本节至此结束. 这一节里我们主要学习了这些内容:

  • public, protected, private, final, static
  • 代词 this
  • 构造函数 ( constructor )

回忆一下, 它们都是什么意思? 怎么用?

下一节我们将介绍 “继承”, 敬请期待 ……