本节关键词: 可访问性控制、修饰符, 代词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
, protected
或 public
中的任何一个!
具体的差异可参看下表:
修饰符 | 当前类 | 同一个包里的类 | 子孙类 | 其他包里的类 |
---|---|---|---|---|
public | YES | YES | YES | YES |
protected | YES | YES | YES | NO |
default | YES | YES | NO | NO |
private | YES | NO | NO | NO |
现在我们成功地把 name
属性私有化了, 那么如果有需要时, 外界怎么对它进行操作呢?
在Pig
类中添加代码, 让它变成下面的样子:
1 | package com.bailey.study.animal; |
我们为 Pig 类添加了2个方法: setName
和 getName
.
这两个方法使用 public
修饰, 这样外界就可以通过这两个方法对 name
属性进行赋值或取值了. ( 相应地, 应该把HelloJava
类中的 p.name = "小花"
改成 p.setName("小花");
)
这样做的好处是: 我们可以灵活控制外界对 name
属性的访问.
例如: 当需要给 name
属性赋值时做一些额外的附加的操作 (如: 数据合法性校验), 就可以把代码写在 setName
方法中. 当然, 如果不想让外界取得name的值, 那把getName方法去掉就行了.
对于 setXXX, getXXX 这样的方法, 通常称为 setter
和 getter
方法.
上面代码中第 10 行很有趣: this.name = name;
其中, this.name
指的是当前对象的name
属性 (在对象 A 中, this.name
就是 A 的name
属性, 而在对象 B 中, 它就是 B 的name
属性). 而 “=” 后面的 name
指的是setName
方法的name
参数 (形参).
这里的代词 this 指向当前对象. 类似地, 还有一个代词: super, 它指向当前类的直接父类.
关于
this
还有另外一层意思, 请继续往下看, 在讲述”构造函数”时将会提及.
构造函数
接下来我们插播一个内容: 构造函数.
所谓”构造函数”其实是一种特别的函数, 它的名称与类名相同, 在实例化的时候构造函数将被首先执行.
通常在构造函数中做一些初始化的工作.
现在在Pig类中添加一个方法, 代码如下:
1 | public Pig(String name, String sex) { |
这样我们为Pig
类添加了一个构造函数. 相应地, 在实例化时就应该传入所需的参数, 例如: Pig p = new Pig("小花", "雄");
赶快试试吧~
与普通方法一样, 构造函数也可以被重载.
试着再为Pig类添加一个构造函数, 代码如下:
1 | public Pig(String 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
修饰符主要是对可访问性 (可视性) 进行控制, 而现在将要讨论的这两个修饰符 ( final
和 static
) 并不影响可访问性.
具体是怎样的? 现在就来看看吧…
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 | public void f (final String param) { |
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 | public class Test { |
其中, static { .... }
大括号内的代码将在首次使用到Test
类的时候被自动执行……
你可以在 HelloJava.java
中写上一句 System.out.println(Test.z);
运行程序试试~ 是不是输出下面的结果了:
1 | static 修饰的代码被执行了... |
说明一下: 上面第 1 行是 “静态代码块” 里输出的, 而第 2 行是新写入的 System.out.println(Test.z); 输出的.
呵呵, 有意思吧……
小结:
(1) static
可以修饰 属性 (静态变量), 方法 (静态方法), 或一段代码.
(2) static
修饰的东西是从属于类的, 生命周期与类相同, 不依赖于实例.
(3) 正因第 (2) 条所述 ( 从属于类, 不依赖于实例 ), 所以… 有可能静态方法或静态代码块被调用/执行的时候还没有任何实例存在. 因此, 不能在静态方法或静态代码块中访问/调用非静态成员 ( 如: 实例变量, 非静态方法 )
static也可以放在内部类的定义前, 这样的类称为静态内部类. 关于什么是内部类, 什么是静态内部类, 它们和普通的类有什么区别, 我们这里暂不展开, 只是提一下 static 还可以使用在类定义时.
好了, 本节至此结束. 这一节里我们主要学习了这些内容:
- public, protected, private, final, static
- 代词 this
- 构造函数 ( constructor )
回忆一下, 它们都是什么意思? 怎么用?
下一节我们将介绍 “继承” ……
Revised on 2021/12/26 04:06:28 by Bailey
-
Next PostJava入门精要 (三)
-
Previous PostJava入门精要 (一)