01,
很久没有思考过什么是面向对象这个问题了, 就好像很久没有吃过烤红薯一样, 那股香味究竟是什么, 已经很难准确地形容出来了. 脑海中只浮现出这样一幅动图:
前两天, 读者秋秋问我:
二哥, 究竟什么是面向对象呢? 还有, 什么是面向过程. 今天去面试的时候, 面试官让我用面向对象的思想谈一谈这次面试的过程.
看到这个问题后, 我思考了好一会儿, 总觉得面试官的问法有点问题: 为什么要用面向对象的思想谈一谈面试的 "过程"?
有点矛盾, 有没有? 先不管这么多了, 且来看看什么是面向对象吧.
一开始的时候, 并没有面向对象, 只有面向过程的概念. 我们回到秋秋面试的话题上, 把面试前 (可以降低需求的复杂性) 的过程简单地拆解一下.
秋秋投递简历
面试官收到秋秋的简历
面试官通知秋秋面试
为了实现这 3 个步骤, 我们定义 3 个方法, 并依次调用:
- qiuqiuDeliverResume();
- interviewerReceiveResume();
- interviewerNotifyQiuqiu();
但是, 假如参加面试的不是秋秋, 这 3 个方法就要重新定义了 (莫抬杠), 尽管步骤并没有变. 面向对象从另一个角度来解决这个问题, 它把对象(对事物的一种抽象描述) 作为程序的基本单元.
回到秋秋面试的例子, 用面向对象的思想来实现, 就需要先定义 2 个类(类是构建对象的蓝图, 里面包含若干的数据和操作这些数据的方法), 分别是应聘者和面试官.
应聘者可以投递简历; 面试官可以接收应聘者的简历和通知应聘者前来面试. 然后再通过类创建两个对象, 分别是秋秋和他的面试官; 对象创建成功后, 就可以依次调用对应的方法完成上述的 3 个步骤.
面向对象 (英语: Object Oriented, 缩写: OO) 思想是一种试图降低代码间的依赖, 应对复杂性, 从而解决代码重用的软件设计思想 -- 恰好解决了面向过程带来的问题.
面向对象有很多重要的特性, 比如说封装, 继承和多态. 这些概念又该怎么理解呢? 所谓一图胜千言, 我给你来一张有趣的, 形象的.
了解了面向对象的思想后, 我们来通过具体的代码完成秋秋面试前的 3 个步骤. 并对类和对象的相关知识点进行归纳和总结.
02,
先来细致地看一下应聘者类 --Candidate.java.
- package com.cmower
- class Candidate {
- private String name;
- public Candidate(String name) {
- this.name = name;
- }
- public void deliverResume() {
- System.out.println(getName() + "发简历");
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
Candidate 包含了类的 4 个重要概念:
成员变量(有时叫做域, 有时叫做字段, 有时叫做属性)name;
成员变量访问器(有时叫做 getter/setter)getName() 和 setName();
构造方法(有时叫做构造器)Candidate();
普通方法 deliverResume().
Candidate 类虽然简单, 但却大有学问.
1)为了保证包名的绝对唯一, Sun 公司建议将域名 (绝对是独一无二的) 以逆序的形式作为包名 -- 这也是为什么包名经常以 org,com 开头的原因(是不是有一种豁然开朗的感觉). 我曾申请过一个域名, 叫 cmower.com, 所以我个人编写的绝大多数代码都是在 com.cmower 包下.
2)类的方法定义顺序依次是: 构造方法> 公有 (public) 方法或保护 (protected) 方法> 私有 (private) 方法> getter/setter 方法.
构造方法是创建对象的必经之路, 放在首位是必须的. 如果只有系统默认的无参构造方法, 可忽略.
公有方法是类的调用者和维护者最关心的方法, 应该在比较靠前的位置展示; 保护方法虽然只有子类关心, 也可能是 "模板设计模式" 下的核心方法, 所以也要靠前; 私有方法只对本类可见, 一般不需要特别关心, 所以往后放; getter/setter 方法承载的信息价值较低, 所以放在类的最后面.
3)setter 方法中, 参数名称与成员变量名称保持一致, 采用 this. 成员名 = 参数名 的形式.
4)成员变量不要用 public 修饰, 尽量用 private 修饰; 如果需要被子类继承, 可以用 protected 修饰.
在初学 Java 编程的时候, 我经常产生一个疑惑: 为什么不使用 public 修饰成员变量呢? 这样做不是比 getter/setter 更方便吗?
我最先想到的答案是这样的:
解释: 如果只有 private String name 而没有 getter/setter 的话, Eclipse 会提示 The value of the field Candidate.name is not used 的警告.
当然了, 这样的答案过于牵强. 那能不能来个靠谱点的答案呢?
能, 为了体现封装的思想: 将数据与行为进行分离. 封装有什么好处呢?
隐藏类的实现细节;
让使用者只能通过事先定制好的方法 (getter/setter) 来访问数据, 可以方便地加入控制方法, 限制对成员变量的不合理操作;
便于修改, 增强代码的维护性和健壮性;
提高代码的安全性和规范性;
使程序更加具备稳定性和可拓展性.
不过, 我对这些严肃的词汇和科学用语实在是提不起半点兴致. 那就再换一个答案吧.
套用《Java 开发实战经典》中举过的一个例子, 我们增加一个应聘者年龄的共有成员变量 age.
- class Candidate {
- public int age;
- }
然后在创建应聘者对象的时候, 直接通过类成员变量赋值: new Candidate().age = -99; 这样赋值是没有任何问题的, 但没有实际的意义, 年龄是不可能为负数的. 为了防止出现这样的错误, 可以对它进行封装, 也就是私有化, 然后在 setter 方法中对年龄进行判断, 代码如下:
- class Candidate {
- private int age;
- public void setAge(int age) {
- if (age >= 0) {
- this.age = age;
- }
- }
- }
这个答案你觉得满意吗? 我最开始看到这个答案的时候觉得很满意. 但看了《阿里巴巴 Java 开发手册》后(详情截图如下), 就觉得不满意了.
第一, 类成员变量使用基本类型很容易造成 NullPointException 的错误; 第二, 在 getter/setter 增加业务逻辑的确很容易把实际的问题隐藏起来.
那, 好的答案究竟是什么呢?
如果设置成员变量为 public, 那么每个调用者都可以读写它, 但如果以 private 配合 getter/setter 的形式访问时, 就可以达到 "不准访问","只读访问","读写访问" 以及 "只写访问" 的目的. 因为不是每个成员变量都需要 getter/setter.
5)每个类都至少会有一个构造方法. 初学者可能会非常疑惑: 我的那个类真的没有构造方法啊!
如果在编写一个类的时候没有编写构造方法, 那么系统就会提供一个无参的构造方法, 就好像是这样:
- class Candidate {
- private String name;
- public Candidate() {
- }
- }
当执行 new Candidate() 的时候, 成员变量 name 就会被初始化为 null. 一般情况下, 我们会为类设置它必须的构造方法, 然后在创建对象的时候对成员变量进行赋值.
03,
再来粗略地看一下面试官类 --Interviewer.java.
- class Interviewer {
- private Candidate candidate;
- public Interviewer (Candidate candidate) {
- this.candidate = candidate;
- }
- public void receviveResume() {
- System.out.println("收到" + getCandidate().getName() + "简历");
- }
- public void notifyInterview() {
- System.out.println("通知" + getCandidate().getName() + "面试");
- }
- public Candidate getCandidate() {
- return candidate;
- }
- public void setCandidate(Candidate candidate) {
- this.candidate = candidate;
- }
- }
Interviewer 有一个成员变量 Candidate, 一个构造方法, 两个共有方法, 以及成员变量对应的 getter/setter.
- (这段代码存在一个严重的问题, 你注意到了吗?)
- 04,
然后, 我们让应聘者发送简历, 让面试官接收简历并发送通知.
- Candidate qiuqiu = new Candidate("秋秋");
- // 发送简历
- qiuqiu.deliverResume();
- Interviewer interviewer = new Interviewer(qiuqiu);
- // 面试官接收到简历
- interviewer.receviveResume();
- // 面试官通知应聘者来面试
- interviewer.notifyInterview();
在初学 Java 的很长一段时间里, 我总是搞不清楚什么是 "对象", 什么是 "引用", 差点因此放弃我的程序生涯. 后来, 在网上认识了一个大佬, 人称老王, 是他挽救了我的程序生涯.
他解释说.
Candidate qiuqiu = new Candidate("秋秋"); 可以拆分为两行代码:
- Candidate qiuqiu;
- qiuqiu = new Candidate("秋秋");
第一行代码 Candidate qiuqiu; 中的 qiuqiu 这时候可以称作是对象变量, 它暂时还没有引用任何对象, 严格意义上, 它也不能称为 null .
第二行代码 qiuqiu = new Candidate("秋秋"); 可以拆分为两个部分,= 号左侧和 = 号右侧.
右侧的表达式 new Candidate("秋秋") 先执行, 执行完后, 会在堆上创建了一个 name 为 "秋秋" 的对象, 类型为 Candidate, 表达式 new Candidate("秋秋") 的值是新创建对象的引用.
然后再把这个引用通过 = 操作符赋值给左侧的对象变量 qiuqiu, 赋值后, qiuqiu 就不再是对象变量了, 应该称为对象引用.
看完老王的解释, 你会不会情不自禁地 "哦, 原来如此啊!" 反正我当时顿悟的时候是这样的.
前面提到, Interviewer 类的设计存在一个严重的问题, 是什么呢?
- Candidate qiuqiu = new Candidate("秋秋");
- Interviewer interviewer = new Interviewer(qiuqiu);
- interviewer.getCandidate().setName("夏夏");
- System.out.println(qiuqiu.getName());
这段代码执行完后, 你会发现秋秋变成了夏夏, 应聘者的私有成员变量 name 竟然被改变了! 问题的原因也很简单, qiuqiu 和 interviewer.getCandidate() 引用了同一个对象.
那怎么解决呢? 当 getter 需要返回一个可变对象的引用时, 应该先进行克隆(clone). 以下展示了一个非常简单的克隆方案.
- class Interviewer {
- private Candidate candidate;
- public Interviewer (Candidate candidate) {
- this.candidate = candidate;
- }
- public Candidate getCandidate() {
- Candidate candidate = new Candidate(this.candidate.getName());
- return candidate;
- }
- }
- 05,
这篇文章花了 5 个多小时才写完, 此刻我的感觉只有一个字 -- 饿, 我要出去吃饭了. 吃饭之前, 我决定先买个烤红薯吃, 重温一下那种久违的香.
来源: https://www.cnblogs.com/qing-gee/p/10670582.html