男孩比女孩需要更多正面管教(转载)# Parenting - 为人父母
c*l
1 楼
【 以下文字转载自 CS 讨论区 】
发信人: amrita (Amrita), 信区: CS
标 题: [分享]:一篇文章讲通一半Java《面向对象一家人》
发信站: BBS 未名空间站 (Thu Oct 24 04:15:04 2013, 美东)
原文链接:http://ladder.azurewebsites.net/forum.php?mod=viewthread&tid=121&extra=page%3D1
第一集:Mr.Method
Hi,大家好~~我是Mr.Method,和Mr. Okay是好哥们儿。
在所有面向对象的语言里你都能见到我的身影。不信?你们学编程的时候第一个程序
都是Hello World吧?那就是依靠对我的调用实现的——无论是哪个平台、哪种语言,
你一定调用了在命令行里输出字符串的某个Method。
铁三角
在正式介绍我自己之前,还是先说说我的哥哥和妹妹吧,我们的家庭背景有助于大家
对我有一个更好的了解。我的哥哥(Mr. Class)和小妹(Mr. Field)加上我,我们仨
可是面向对象家庭里绝对的主角、“铁三角”。我们三个之所以称为“铁三角”是因为
我们各司其职、配合的非常默契。Mr.Class,他是个很好的组织者,他能把从现实世界
抽象出来的结果良好地组织在一起(这个叫“封装”)——我(Mr.Method)和我小妹
(Ms. Field)被我哥管着(注:被封装在类里)。小妹负责表示我们这个小组织当前
的状态,而我负责我们这个小组织能干什么。有的时候小妹的状态会决定我能不能做一
些事——比如我想买个什么东西,小妹说:“没钱!”那我就不能执行了。而有的时候
我的行为也会影响到小妹的状态——比如我买了辆新车,那么就会从小妹那里减去一些
钱。女孩子家,怕她受伤受骗什么的,一般情况下我们不会让外人直接接触小妹,所以
,保护小妹的事情也由我来负责。总之呢,你知道我在这个小组织里是个跑腿儿的(表
示这个组织能干什么)、同时还是小妹的保护伞,这就足够了。
我的前身
面向对象编程在软件界算是个划时代的进步,之前那个时代叫“面向过程时代”,现
在仍然有面向过程的程序在编写,但远不像以前那么多了。回退二三十年,面向过程流
行的年代,那时候我还不叫Mr.Method,那时候我叫Mr.Funtion。后来有了面向对象,
Function被类所封装管理,于是我们被称为“类的方法”(methods of class),用来
表示这个类能做些什么事情。因为我们是类的成员,被封装在类里,所以从面向过程年
代过来的老程序员们仍然喜欢叫我们Mr. Member Function(成员函数),每当听到这
个名字,我都感觉好好亲切……好好感动……好好内牛满面。算了……过去的事情了。
因为我表达的是“一个类能干什么”,所以,程序中真正做事的——是我。我之所以
能“做事”,全靠程序员们为我编写的代码逻辑(method body),也就是——算法。
没有算法,我就是个空壳;算法错误,程序就会有bug。如果你想省事,不妨这样记:
Mr.Method==算法,或者算法==Mr.Method。
我的样子
在不同的面向对象语言里,我的样子可能不太一样。就自我感觉而言,我还是认为我
在Java/C#/C++语言里的样子最帅。所以,今天就聊聊我在Java语言里长什么样吧(注
:方法在Java/C#/C++的格式几乎没有区别),谁让你们都想靠学Java找工作、攻学位
呢!
Well,当你在代码里见到我的时候,要么是在定义(define)我,要么是在调用(call
)我。因为定义我时候的样子决定了我被调用时的样子,所以让我们从定义开始。
在观察我的样子之前,请你把我想象成一个“加工厂”。比如,果酱加工厂,你给它
输入水果作为原料,它的内部运转加工,最后把加工好的果酱还给你。当然了,我不负
责加工果酱,我加工的是——数据。
OK,来看我被定义时的样子,从前往后依次是:
1. 返回值类型:直截了当地告诉你我的产品是什么。不过,有的时候我并没有产出,
而是默默地做完某些事情(比如阻挡不速之客访问我小妹的值),这时候为了统一起见
,我也会有一个看上去像是返回值类型的void摆在那里。我之所以说在Java/C#/C++里
长的帅,就是因为这些语言在语法格式上的统一。
2. 名字:我的名字必需符合Java语言的符号命名规范,而且使用用caml格式(注:在C
#里使用Pascal格式,C#的发明人Anders先生将以Pascal语言为蓝本的DELPHI开发环境
的一些特性引入了C#,使其语言格式看上去更优雅;而Java则保留了一些C++中的原始
美)。
3. 参数列表:由一对括号括起来的一组“类型->参数名”对,参数列表中的参数可以
是0个、1个、2个...当参数为0个时,也可以说这个方法没有参数;当参数多于1个时,
“类型->参数”对之间要用逗号分隔。
4. 方法体:由一对花括号包围,方法体里就是对数据的加工逻辑了,也就是真正的算
法。翻过头去看下第1项:如果这个方法有返回值(不是void),那么方法体里必需保
证有return语句返回一个与返回值类型相匹配的值(思考:为什么我用的是“相匹配”
而不是“一样”?)。
另外,有的时候我也喜欢打扮一下自己、在整个定义之前加几个修饰符(modifier)
什么的,最常见的就是public或private与static的组合。换句话说,这四种修饰占到
80%以上的情况——(1)public, (2)private, (3)public static, (4)private static
。具体这四种修饰是什么意思,后面会讲到,现在你不必特别在意。
下面是我被定义时常见的样子,请你们自己把上面说的四个部分找出来吧!
================================================
// 无返回值,无参数
void sayHello() {
// 逻辑
}
// 无返回值,1个参数
void sayHello(String friendName){
// 逻辑
}
// 有返回值,1个参数
double convertDollarToRmb(double dollarValue){
double rmbValue = 0;
// 逻辑
return rmbValue;
}
// 有返回值,2个参数
double add(double input1, double input2)
{
double result = input1+input2;
return result;
}
================================================
知道了我在定义的时候长什么样子,那么有一件事就要记清楚了:我的定义只可能出现
在类体里,也就是类的那层花括号里。换句话说,无论我被定义在类体之外,还是在其
它方法体的内部,编译器都不会让你通过的。我在C#里跟在Java里差不多,但在C++里
有那么一点变化,就是我仍然可以被定义在类体之外,这时候我被称为“全局方法”或
“全局函数”,这体现了C++的一种贴近底层的“原始美”,但跟我们学Java的没什么
关系啦^^。(思考:main方法也是一个方法,你能找出它的四个部分吗?)
说完了我被定义时的样子,再来说说我被调用的时候长什么样子吧——看上去跟被定
义的时候长的差不多,但一定要记住,我只能在方法体里被调用(即方法的调用一定只
能是在其它方法的方法体里出现)。科学点讲,当方法B被方法A调用时,方法B出现在
方法A的方法体里,构成了方法A算法的一部分;这时方法A被称为调用者(caller),
方法B被称为被调者(callee);如果方法B又去调用了方法C,那么会就形成A-->B-->C
这样一个调用层级,学名叫做调用栈(call stack)。
下面是上面四个方法定义在被调用时的样子:
================================================
void callAllMethods()
{
sayHello();
sayHello("Mr.Okay");
double rmb = convertDollarToRmb(1000000);
double r = add(1.5, 3.7);
add(9.9, 0.1); // 这是可以的,但没意义
}
================================================
仔细看,你会发现几处需要注意的地方:
1. 我在被调用的时候,参数的类型就不用写了,因为那是定义我的时候所用的语法,
不是调用的语法,这里我见过太多初学者写错了。
2. 对于有返回值的方法,你需要搞一个变量来接收这个返回值。当然了,不接收也可
以,但这样做就没什么意义了。以前有过一种这样风格的方法定义,但现代编程格式已
经渐渐把这种方法淘汰了。
3. 就算一个方法没有参数,调用我的时候名字后面的那对括号也不能省略,因为你省
略了,编译器会认为你只是提到了我的名字,而没有“调用”我。()
我的参数
Well,我知道有很多人纠结于“为什么在调用的时候不用写参数类型呀?” “调用的
时候传进去的参数名字是不是必需要和定义时的一样呀?” 这类问题。只要明白了我
的参数,那么这些问题就都明白了。简单讲,我在被定义的时候,参数列表(就是名字
后圆括号里的“类型->参数名”对)叫“形式参数”;而我在被调用的时候,传给我的
参数是真的要被操作的数据,所以叫“实际参数”。下面我来给大家解释解释。
所谓“形式参数”,就是走走形式嘛~~没什么了不起的。这里说的“走走形式”指的
是这些形式参数要参与到我被定义的算法里,在算法里“走走形式”。比如,有这样一
个方法(用伪码表示):
================
果酱 果酱机(水果 某个水果,干果 某个干果,调料 主调料, 调料 配料)
{
把某个水果切碎磨细;
把某个干果切成粒;
加入主调料;
加入配料;
混合均匀;
返回果酱;
}
=================
这里大家看到的是,“某个水果”、“某个干果”、“主调料”和“配料”都作为跑龙
套的形式参数参与到了果酱的生产算法中。
下面再看调用,看起来是这样(必需被其它的方法所调用):
=================
void 做果酱()
{
水果 我的水果=买来一个苹果;
干果 我的干果=上回剩下的一包杏仁;
调料 甜料=家里的砂糖;
调料 酸料=冰箱里的柠檬汁;
果酱 新做的果酱=果酱机(我的水果,我的干果,甜料,酸料);
吃果酱;
吃果酱;
吃果酱;
吃果酱;
// ...
上医院;
}
==================
现在大家应该明白了吧?当调用果酱机方法做果酱的时候,我必需得投入真材实料才能
做出真的果酱来,也就是让先前定义好的算法得以执行。因为是真材实料嘛,所以叫“
实际参数”,简称“实参”。
因为在方法定义的时候就已经要求了参数的类型,所以在调用的时候你只需要把类型
匹配的实参传进方法就行了,不必再次写参数的类型。
顺带跟大家说一下,大家可能一上Java课的时候就听说过“Java是强类型语言”这句
话,这句话怎么理解呢?
关于强类型
想象一下如果这样调用果酱机方法,
=================
void 做果酱()
{
石头 我的石头=拣来一块石头;
沙子 我的沙子=挖来一袋沙子;
调料 甜料=家里的砂糖;
调料 酸料=冰箱里的柠檬汁;
果酱 新做的果酱=果酱机(我的石头,我的沙子,甜料,酸料);// 你玩儿我?
?!!
吃果酱;
吃果酱;
吃果酱;
吃果酱;
// ...
上医院;// 死翘翘
}
==================
My god!这做出来的东西能吃吗?
你别说,如果这是个C/C++函数(function),说不定真能把“果酱”给你做出来,也
就是说,函数在执行的时候不会报错,而且能返回给你结果,但如果你想拿这个结果做
点什么事情,那结果是不可预知的。这也就是我们常说的C/C++是类型不安全的(现代C
++要好很多)。但如果果酱机方法是用Java语言写的,因为Java是类型安全的语言,所
以你在做这个调用的时候“果酱 新做的果酱=果酱机(我的石头,我的沙子,甜料,酸
料);”编译器就不让你通过,因为你给的实参类型与定义时的形参类型不匹配。
说说重载(overload)
问你个问题:在同一个类里定义方法的时候,方法名可以相同吗?答案是:“可以!
”但有个要求——方法名相同的时候参数列表不可以再相同。这种方法名相同、参数列
表不同的情况称为方法的重载。前面定义的两个sayHello方法就构成了重载。
这里有几点需要注意的:
1. 参数列表不同,包括了参数的个数不同、类型不同。比如:void myMethod(String
input1, double input2){...}和void myMethod(double input1, String input2){...
}就构成重载。
2. 只改参数名是不能构成重载的。比如:void myMethod(String input1, double
input2){...}和void myMethod(String arg1, double arg2){...}是不重载的。
3. 只改返回值类型也不能构成重载。比如:void myMethod(String input1, double
input2){...}和double myMethod(String input1, double input2){...}是不重载的。
其实大家应该已经发现了,决定两个方法是否重载的是方法定义四个部分中的方法名加
参数列表部分。所以,这两个部分(方法名+参数列表)又称为“方法签名”(method
signature),也就是这两部分的组合确定了一个方法的唯一性。
说说重写(override)
按理说重写这事儿非常简单,但它却与面向对象最核心的一个概念“多态”搅和在一
起了,所以在有些神话传说里会把重写和多态写的云里雾里、神乎其神。其实别听那套
~~很简单的。
咱们先不理多态的茬,只说重写。它的概念是:如果在父类里定义了一个方法A,在这
个父类的子类里你再次定义了方法A,它的返回值类型、方法名、参数列表与父类中的
方法A完全一致,那么我们就说子类中的方法A就对父类中的方法A进行了重写。这再好
理解不过了——方法定义的前三部分安全一样,只有方法体不一样,当然就是子类方法
把父类方法的算法逻辑给覆盖重写了。看例子:
=================
public class Person
{
public void speak()
{
System.out.println("I'm a person.");
}
}
public class Student extends Person
{
public void speak()
{
System.out.println("I'm a student.");
}
}
public class Teacher extends Person
{
public void speak()
{
System.out.println("I'm a teacher.");
}
}
=================
例子中的Person类是Student类和Teacher类的父类(倒过来说,Student和Teacher是
Person的子类或派生类)。它们都拥有返回值类型、名字、参数列表完全相同的方法
speak,所以,Student类和Teacher类里的speak方法对Person类的speak方法构成了重
写。
切记一句话:重写用的好,是福;用不好,是项目里最大的祸害。
多态(polymorphism)——事情有点儿复杂了
什么是多态呢?它可以算得上是OO的重中之重了,它可以这么定义——当用一个父类
类型的变量引用一个子类类型的实例时,通过父类类型的变量调用一个被重载的方法时
,被执行的总是被子类所重写后的版本。看!多态是以重写为基础的。单看定义是很难
明白的,让我们看个例子。
首先,我们在“重写”例子代码上再加一个类:
=================
public class CseTeacher extends Teacher
{
public void speak()
{
System.out.println("I'm a CSE teacher.");
}
}
=================
这个CseTeacher类对Teacher类的speak做了重写(这是再度重写了)。那么,下面这组
方法调用的输出是什么呢?
public class Program
{
public static void main(String[] args)
{
Person person; // 父类类型变量,请注意,它只是个变量,不是实例
person = new Person(); // 变量与实例的类型一致
person.speak();
person = new Student(); // 父类类型变量引用子类实例
person.speak();
person = new Teacher(); // 父类类型变量引用子类实例
person.speak();
person = new CseTeacher(); // 父类类型变量引用子类实例
person.speak();
Teacher teacher = new CseTeacher(); // 父类类型变量引用子类实例
teacher.speak();
}
}
答案是:
I'm a person.
I'm a student.
I'm a teacher.
I'm a CSE teacher.
I'm a CSE teacher.
===================
多态背后的秘密——绑定(bind)
多态是不是很神奇啊?我来向你透露一点它的秘密吧!不过,在我透露这个秘密之前
,我先说一下版权的事儿:咳咳,本系列原创文章发表在http://ladder.azurewebsites.net,转载的话请注明出处,如果你喜欢,欢迎在我们的论坛注册或加入QQ群277252742共同学习提高。
好了,现在开始说秘密:这个秘密就叫“绑定”。什么是绑定呢?如果你仔细观察,
会发现Java里每个方法都必需通过它的拥有者来调用:比如person.speak(),Double.
valueOf(),this.doSomething()。这种把我和我的拥有者关联起来的行为就叫做绑定
,在Java语言里,我被绑定给谁是在编译器编译代码的时候就确定了的,这种绑定方式
也叫“编译期绑定”或者“早绑定”。比如,当你看到我的定义中有static修饰符的时
候,我就被绑定到定义我的类上,而不是这个类的实例上(Double.valueOf就是个典型
的例子);当我不被static修饰的时候,我会被绑定给定义我的类的实例上——我总跟
定义我的类的实例直接关联,所以才会产生多态的效果,这是编译器刻意追求的效果。
聪明的你一定会问:“既然有早绑定,那么一定有晚绑定咯?”你说对了!晚绑定指
的是方法与哪个对象想关联不由编译器来确定,而是在程序运行的时候根据程序逻辑确
定,晚绑定也叫运行期绑定。
因为编译期程序还没有执行,所以也叫静态期(static);程序运行起来之后又叫动
态期(dynamic),所以,这两种绑定又被分别称为“静态绑定”和“动态绑定”。听
好了:一门编程语言采用何种绑定决定了它是静态语言还是动态语言。像C/C++/Java/C
#等语言都是静态语言(因为它们需要做静态绑定,所以编译器要提前知道数据的类型
,所以静态语言往往是强类型的);而像JavaScript/Python/Perl等脚本语言,则是动
态语言,因为采用了动态绑定,它们的对象非常灵活多变(当然,也非常容易把程序搞
乱)。
public与private
最后说说两个常见的修饰符吧。特简单~~当我被public修饰的时候,你在别的类的方法
里用能通过与我绑定的类或实例调用我;当我被private修饰的时候,在别的类的方法
里你是看不见我的。如果我没被修饰符所修饰,默认就是private啦~~他们怕我出去惹
事,所以默认把我关在屋里
如果以后你有机会学习C#,你还会遇到virtual修饰符和override修饰符,这俩是一对
儿。父类的方法如果用virtual修饰了,子类标记了override的方法才能重写父类的方
法……还是Java简单,方法重写是天生的。
哦,差点忘了abstract,当我被这个修饰符修饰的时候可省事啦!这时候你只需要定
义我的返回值类型、名字和参数列表,方法体完全不用定义——这时候我只是个抽象的
签名,没有任何实际的算法。这时候的我被称为“抽象方法”,包含我的类叫“抽象类
”,因为我没有具体的算法,所以这个类也因为我的拖累而不完整了。抽象类可以用来
声明变量,但不能用来创建实例——你想啊,如果创建了实例,你又用实例去调用一个
没定义完整的方法,不崩溃才怪!所以,干脆不让你用抽象类来创建实例了。如果你从
抽象类派生了子类,并且在子类里实现(注意:这时候不叫重写,因为抽象方法本身没
有实际的算法,你不可以“重”写)了抽象方法,那它就成一个完成的实现类(
concrete class)了。别看抽象类和抽象方法是“不完整”和“未定义算法”的,但就
是因为它们的不完整使得他们在整个OO设计中有着举足轻重的地位……要领悟这一点,
你还有很长的路要走,大概得写五万行代码吧
这让我想起了那位神龙见首不见尾、虚无飘渺的Ms.Interface——这老姐更是抽象到
皮包骨头,女人嘛,瘦即是可爱……要不为什么架构师们爱她爱到发疯呢。她的事儿,
以后再说吧。
发信人: amrita (Amrita), 信区: CS
标 题: [分享]:一篇文章讲通一半Java《面向对象一家人》
发信站: BBS 未名空间站 (Thu Oct 24 04:15:04 2013, 美东)
原文链接:http://ladder.azurewebsites.net/forum.php?mod=viewthread&tid=121&extra=page%3D1
第一集:Mr.Method
Hi,大家好~~我是Mr.Method,和Mr. Okay是好哥们儿。
在所有面向对象的语言里你都能见到我的身影。不信?你们学编程的时候第一个程序
都是Hello World吧?那就是依靠对我的调用实现的——无论是哪个平台、哪种语言,
你一定调用了在命令行里输出字符串的某个Method。
铁三角
在正式介绍我自己之前,还是先说说我的哥哥和妹妹吧,我们的家庭背景有助于大家
对我有一个更好的了解。我的哥哥(Mr. Class)和小妹(Mr. Field)加上我,我们仨
可是面向对象家庭里绝对的主角、“铁三角”。我们三个之所以称为“铁三角”是因为
我们各司其职、配合的非常默契。Mr.Class,他是个很好的组织者,他能把从现实世界
抽象出来的结果良好地组织在一起(这个叫“封装”)——我(Mr.Method)和我小妹
(Ms. Field)被我哥管着(注:被封装在类里)。小妹负责表示我们这个小组织当前
的状态,而我负责我们这个小组织能干什么。有的时候小妹的状态会决定我能不能做一
些事——比如我想买个什么东西,小妹说:“没钱!”那我就不能执行了。而有的时候
我的行为也会影响到小妹的状态——比如我买了辆新车,那么就会从小妹那里减去一些
钱。女孩子家,怕她受伤受骗什么的,一般情况下我们不会让外人直接接触小妹,所以
,保护小妹的事情也由我来负责。总之呢,你知道我在这个小组织里是个跑腿儿的(表
示这个组织能干什么)、同时还是小妹的保护伞,这就足够了。
我的前身
面向对象编程在软件界算是个划时代的进步,之前那个时代叫“面向过程时代”,现
在仍然有面向过程的程序在编写,但远不像以前那么多了。回退二三十年,面向过程流
行的年代,那时候我还不叫Mr.Method,那时候我叫Mr.Funtion。后来有了面向对象,
Function被类所封装管理,于是我们被称为“类的方法”(methods of class),用来
表示这个类能做些什么事情。因为我们是类的成员,被封装在类里,所以从面向过程年
代过来的老程序员们仍然喜欢叫我们Mr. Member Function(成员函数),每当听到这
个名字,我都感觉好好亲切……好好感动……好好内牛满面。算了……过去的事情了。
因为我表达的是“一个类能干什么”,所以,程序中真正做事的——是我。我之所以
能“做事”,全靠程序员们为我编写的代码逻辑(method body),也就是——算法。
没有算法,我就是个空壳;算法错误,程序就会有bug。如果你想省事,不妨这样记:
Mr.Method==算法,或者算法==Mr.Method。
我的样子
在不同的面向对象语言里,我的样子可能不太一样。就自我感觉而言,我还是认为我
在Java/C#/C++语言里的样子最帅。所以,今天就聊聊我在Java语言里长什么样吧(注
:方法在Java/C#/C++的格式几乎没有区别),谁让你们都想靠学Java找工作、攻学位
呢!
Well,当你在代码里见到我的时候,要么是在定义(define)我,要么是在调用(call
)我。因为定义我时候的样子决定了我被调用时的样子,所以让我们从定义开始。
在观察我的样子之前,请你把我想象成一个“加工厂”。比如,果酱加工厂,你给它
输入水果作为原料,它的内部运转加工,最后把加工好的果酱还给你。当然了,我不负
责加工果酱,我加工的是——数据。
OK,来看我被定义时的样子,从前往后依次是:
1. 返回值类型:直截了当地告诉你我的产品是什么。不过,有的时候我并没有产出,
而是默默地做完某些事情(比如阻挡不速之客访问我小妹的值),这时候为了统一起见
,我也会有一个看上去像是返回值类型的void摆在那里。我之所以说在Java/C#/C++里
长的帅,就是因为这些语言在语法格式上的统一。
2. 名字:我的名字必需符合Java语言的符号命名规范,而且使用用caml格式(注:在C
#里使用Pascal格式,C#的发明人Anders先生将以Pascal语言为蓝本的DELPHI开发环境
的一些特性引入了C#,使其语言格式看上去更优雅;而Java则保留了一些C++中的原始
美)。
3. 参数列表:由一对括号括起来的一组“类型->参数名”对,参数列表中的参数可以
是0个、1个、2个...当参数为0个时,也可以说这个方法没有参数;当参数多于1个时,
“类型->参数”对之间要用逗号分隔。
4. 方法体:由一对花括号包围,方法体里就是对数据的加工逻辑了,也就是真正的算
法。翻过头去看下第1项:如果这个方法有返回值(不是void),那么方法体里必需保
证有return语句返回一个与返回值类型相匹配的值(思考:为什么我用的是“相匹配”
而不是“一样”?)。
另外,有的时候我也喜欢打扮一下自己、在整个定义之前加几个修饰符(modifier)
什么的,最常见的就是public或private与static的组合。换句话说,这四种修饰占到
80%以上的情况——(1)public, (2)private, (3)public static, (4)private static
。具体这四种修饰是什么意思,后面会讲到,现在你不必特别在意。
下面是我被定义时常见的样子,请你们自己把上面说的四个部分找出来吧!
================================================
// 无返回值,无参数
void sayHello() {
// 逻辑
}
// 无返回值,1个参数
void sayHello(String friendName){
// 逻辑
}
// 有返回值,1个参数
double convertDollarToRmb(double dollarValue){
double rmbValue = 0;
// 逻辑
return rmbValue;
}
// 有返回值,2个参数
double add(double input1, double input2)
{
double result = input1+input2;
return result;
}
================================================
知道了我在定义的时候长什么样子,那么有一件事就要记清楚了:我的定义只可能出现
在类体里,也就是类的那层花括号里。换句话说,无论我被定义在类体之外,还是在其
它方法体的内部,编译器都不会让你通过的。我在C#里跟在Java里差不多,但在C++里
有那么一点变化,就是我仍然可以被定义在类体之外,这时候我被称为“全局方法”或
“全局函数”,这体现了C++的一种贴近底层的“原始美”,但跟我们学Java的没什么
关系啦^^。(思考:main方法也是一个方法,你能找出它的四个部分吗?)
说完了我被定义时的样子,再来说说我被调用的时候长什么样子吧——看上去跟被定
义的时候长的差不多,但一定要记住,我只能在方法体里被调用(即方法的调用一定只
能是在其它方法的方法体里出现)。科学点讲,当方法B被方法A调用时,方法B出现在
方法A的方法体里,构成了方法A算法的一部分;这时方法A被称为调用者(caller),
方法B被称为被调者(callee);如果方法B又去调用了方法C,那么会就形成A-->B-->C
这样一个调用层级,学名叫做调用栈(call stack)。
下面是上面四个方法定义在被调用时的样子:
================================================
void callAllMethods()
{
sayHello();
sayHello("Mr.Okay");
double rmb = convertDollarToRmb(1000000);
double r = add(1.5, 3.7);
add(9.9, 0.1); // 这是可以的,但没意义
}
================================================
仔细看,你会发现几处需要注意的地方:
1. 我在被调用的时候,参数的类型就不用写了,因为那是定义我的时候所用的语法,
不是调用的语法,这里我见过太多初学者写错了。
2. 对于有返回值的方法,你需要搞一个变量来接收这个返回值。当然了,不接收也可
以,但这样做就没什么意义了。以前有过一种这样风格的方法定义,但现代编程格式已
经渐渐把这种方法淘汰了。
3. 就算一个方法没有参数,调用我的时候名字后面的那对括号也不能省略,因为你省
略了,编译器会认为你只是提到了我的名字,而没有“调用”我。()
我的参数
Well,我知道有很多人纠结于“为什么在调用的时候不用写参数类型呀?” “调用的
时候传进去的参数名字是不是必需要和定义时的一样呀?” 这类问题。只要明白了我
的参数,那么这些问题就都明白了。简单讲,我在被定义的时候,参数列表(就是名字
后圆括号里的“类型->参数名”对)叫“形式参数”;而我在被调用的时候,传给我的
参数是真的要被操作的数据,所以叫“实际参数”。下面我来给大家解释解释。
所谓“形式参数”,就是走走形式嘛~~没什么了不起的。这里说的“走走形式”指的
是这些形式参数要参与到我被定义的算法里,在算法里“走走形式”。比如,有这样一
个方法(用伪码表示):
================
果酱 果酱机(水果 某个水果,干果 某个干果,调料 主调料, 调料 配料)
{
把某个水果切碎磨细;
把某个干果切成粒;
加入主调料;
加入配料;
混合均匀;
返回果酱;
}
=================
这里大家看到的是,“某个水果”、“某个干果”、“主调料”和“配料”都作为跑龙
套的形式参数参与到了果酱的生产算法中。
下面再看调用,看起来是这样(必需被其它的方法所调用):
=================
void 做果酱()
{
水果 我的水果=买来一个苹果;
干果 我的干果=上回剩下的一包杏仁;
调料 甜料=家里的砂糖;
调料 酸料=冰箱里的柠檬汁;
果酱 新做的果酱=果酱机(我的水果,我的干果,甜料,酸料);
吃果酱;
吃果酱;
吃果酱;
吃果酱;
// ...
上医院;
}
==================
现在大家应该明白了吧?当调用果酱机方法做果酱的时候,我必需得投入真材实料才能
做出真的果酱来,也就是让先前定义好的算法得以执行。因为是真材实料嘛,所以叫“
实际参数”,简称“实参”。
因为在方法定义的时候就已经要求了参数的类型,所以在调用的时候你只需要把类型
匹配的实参传进方法就行了,不必再次写参数的类型。
顺带跟大家说一下,大家可能一上Java课的时候就听说过“Java是强类型语言”这句
话,这句话怎么理解呢?
关于强类型
想象一下如果这样调用果酱机方法,
=================
void 做果酱()
{
石头 我的石头=拣来一块石头;
沙子 我的沙子=挖来一袋沙子;
调料 甜料=家里的砂糖;
调料 酸料=冰箱里的柠檬汁;
果酱 新做的果酱=果酱机(我的石头,我的沙子,甜料,酸料);// 你玩儿我?
?!!
吃果酱;
吃果酱;
吃果酱;
吃果酱;
// ...
上医院;// 死翘翘
}
==================
My god!这做出来的东西能吃吗?
你别说,如果这是个C/C++函数(function),说不定真能把“果酱”给你做出来,也
就是说,函数在执行的时候不会报错,而且能返回给你结果,但如果你想拿这个结果做
点什么事情,那结果是不可预知的。这也就是我们常说的C/C++是类型不安全的(现代C
++要好很多)。但如果果酱机方法是用Java语言写的,因为Java是类型安全的语言,所
以你在做这个调用的时候“果酱 新做的果酱=果酱机(我的石头,我的沙子,甜料,酸
料);”编译器就不让你通过,因为你给的实参类型与定义时的形参类型不匹配。
说说重载(overload)
问你个问题:在同一个类里定义方法的时候,方法名可以相同吗?答案是:“可以!
”但有个要求——方法名相同的时候参数列表不可以再相同。这种方法名相同、参数列
表不同的情况称为方法的重载。前面定义的两个sayHello方法就构成了重载。
这里有几点需要注意的:
1. 参数列表不同,包括了参数的个数不同、类型不同。比如:void myMethod(String
input1, double input2){...}和void myMethod(double input1, String input2){...
}就构成重载。
2. 只改参数名是不能构成重载的。比如:void myMethod(String input1, double
input2){...}和void myMethod(String arg1, double arg2){...}是不重载的。
3. 只改返回值类型也不能构成重载。比如:void myMethod(String input1, double
input2){...}和double myMethod(String input1, double input2){...}是不重载的。
其实大家应该已经发现了,决定两个方法是否重载的是方法定义四个部分中的方法名加
参数列表部分。所以,这两个部分(方法名+参数列表)又称为“方法签名”(method
signature),也就是这两部分的组合确定了一个方法的唯一性。
说说重写(override)
按理说重写这事儿非常简单,但它却与面向对象最核心的一个概念“多态”搅和在一
起了,所以在有些神话传说里会把重写和多态写的云里雾里、神乎其神。其实别听那套
~~很简单的。
咱们先不理多态的茬,只说重写。它的概念是:如果在父类里定义了一个方法A,在这
个父类的子类里你再次定义了方法A,它的返回值类型、方法名、参数列表与父类中的
方法A完全一致,那么我们就说子类中的方法A就对父类中的方法A进行了重写。这再好
理解不过了——方法定义的前三部分安全一样,只有方法体不一样,当然就是子类方法
把父类方法的算法逻辑给覆盖重写了。看例子:
=================
public class Person
{
public void speak()
{
System.out.println("I'm a person.");
}
}
public class Student extends Person
{
public void speak()
{
System.out.println("I'm a student.");
}
}
public class Teacher extends Person
{
public void speak()
{
System.out.println("I'm a teacher.");
}
}
=================
例子中的Person类是Student类和Teacher类的父类(倒过来说,Student和Teacher是
Person的子类或派生类)。它们都拥有返回值类型、名字、参数列表完全相同的方法
speak,所以,Student类和Teacher类里的speak方法对Person类的speak方法构成了重
写。
切记一句话:重写用的好,是福;用不好,是项目里最大的祸害。
多态(polymorphism)——事情有点儿复杂了
什么是多态呢?它可以算得上是OO的重中之重了,它可以这么定义——当用一个父类
类型的变量引用一个子类类型的实例时,通过父类类型的变量调用一个被重载的方法时
,被执行的总是被子类所重写后的版本。看!多态是以重写为基础的。单看定义是很难
明白的,让我们看个例子。
首先,我们在“重写”例子代码上再加一个类:
=================
public class CseTeacher extends Teacher
{
public void speak()
{
System.out.println("I'm a CSE teacher.");
}
}
=================
这个CseTeacher类对Teacher类的speak做了重写(这是再度重写了)。那么,下面这组
方法调用的输出是什么呢?
public class Program
{
public static void main(String[] args)
{
Person person; // 父类类型变量,请注意,它只是个变量,不是实例
person = new Person(); // 变量与实例的类型一致
person.speak();
person = new Student(); // 父类类型变量引用子类实例
person.speak();
person = new Teacher(); // 父类类型变量引用子类实例
person.speak();
person = new CseTeacher(); // 父类类型变量引用子类实例
person.speak();
Teacher teacher = new CseTeacher(); // 父类类型变量引用子类实例
teacher.speak();
}
}
答案是:
I'm a person.
I'm a student.
I'm a teacher.
I'm a CSE teacher.
I'm a CSE teacher.
===================
多态背后的秘密——绑定(bind)
多态是不是很神奇啊?我来向你透露一点它的秘密吧!不过,在我透露这个秘密之前
,我先说一下版权的事儿:咳咳,本系列原创文章发表在http://ladder.azurewebsites.net,转载的话请注明出处,如果你喜欢,欢迎在我们的论坛注册或加入QQ群277252742共同学习提高。
好了,现在开始说秘密:这个秘密就叫“绑定”。什么是绑定呢?如果你仔细观察,
会发现Java里每个方法都必需通过它的拥有者来调用:比如person.speak(),Double.
valueOf(),this.doSomething()。这种把我和我的拥有者关联起来的行为就叫做绑定
,在Java语言里,我被绑定给谁是在编译器编译代码的时候就确定了的,这种绑定方式
也叫“编译期绑定”或者“早绑定”。比如,当你看到我的定义中有static修饰符的时
候,我就被绑定到定义我的类上,而不是这个类的实例上(Double.valueOf就是个典型
的例子);当我不被static修饰的时候,我会被绑定给定义我的类的实例上——我总跟
定义我的类的实例直接关联,所以才会产生多态的效果,这是编译器刻意追求的效果。
聪明的你一定会问:“既然有早绑定,那么一定有晚绑定咯?”你说对了!晚绑定指
的是方法与哪个对象想关联不由编译器来确定,而是在程序运行的时候根据程序逻辑确
定,晚绑定也叫运行期绑定。
因为编译期程序还没有执行,所以也叫静态期(static);程序运行起来之后又叫动
态期(dynamic),所以,这两种绑定又被分别称为“静态绑定”和“动态绑定”。听
好了:一门编程语言采用何种绑定决定了它是静态语言还是动态语言。像C/C++/Java/C
#等语言都是静态语言(因为它们需要做静态绑定,所以编译器要提前知道数据的类型
,所以静态语言往往是强类型的);而像JavaScript/Python/Perl等脚本语言,则是动
态语言,因为采用了动态绑定,它们的对象非常灵活多变(当然,也非常容易把程序搞
乱)。
public与private
最后说说两个常见的修饰符吧。特简单~~当我被public修饰的时候,你在别的类的方法
里用能通过与我绑定的类或实例调用我;当我被private修饰的时候,在别的类的方法
里你是看不见我的。如果我没被修饰符所修饰,默认就是private啦~~他们怕我出去惹
事,所以默认把我关在屋里
如果以后你有机会学习C#,你还会遇到virtual修饰符和override修饰符,这俩是一对
儿。父类的方法如果用virtual修饰了,子类标记了override的方法才能重写父类的方
法……还是Java简单,方法重写是天生的。
哦,差点忘了abstract,当我被这个修饰符修饰的时候可省事啦!这时候你只需要定
义我的返回值类型、名字和参数列表,方法体完全不用定义——这时候我只是个抽象的
签名,没有任何实际的算法。这时候的我被称为“抽象方法”,包含我的类叫“抽象类
”,因为我没有具体的算法,所以这个类也因为我的拖累而不完整了。抽象类可以用来
声明变量,但不能用来创建实例——你想啊,如果创建了实例,你又用实例去调用一个
没定义完整的方法,不崩溃才怪!所以,干脆不让你用抽象类来创建实例了。如果你从
抽象类派生了子类,并且在子类里实现(注意:这时候不叫重写,因为抽象方法本身没
有实际的算法,你不可以“重”写)了抽象方法,那它就成一个完成的实现类(
concrete class)了。别看抽象类和抽象方法是“不完整”和“未定义算法”的,但就
是因为它们的不完整使得他们在整个OO设计中有着举足轻重的地位……要领悟这一点,
你还有很长的路要走,大概得写五万行代码吧
这让我想起了那位神龙见首不见尾、虚无飘渺的Ms.Interface——这老姐更是抽象到
皮包骨头,女人嘛,瘦即是可爱……要不为什么架构师们爱她爱到发疯呢。她的事儿,
以后再说吧。