什么叫面向对象(Object Oriented,简称OO)?
你知道了JAVA虚拟机、编写了那么多的代码、也知道怎么编译、运行JAVA代码了,如果就到此为止,你就还没有窥探到JAVA的门道!换句话来讲,还没开始学习JAVA!
因为JAVA是面向对象的编程语言(OOP:Object-oriented Programming),我们到现在为止,还没有怎么接触面向对象的概念,所以,就等于还没开始学习JAVA!
顾名思义,所谓面向对象,就是一切以对象为核心!不管是面向过程,还是面向对象,都是为了解决问题而存在的一种思维方式!就是怎么样去理解和解决一个问题。世间万物皆对象。我们理解一个新的概念通常用类比的方法。现在我使用一个类比物:电脑。现在这里,每个人一台电脑,电脑就是一个对象。这台电脑是我用的,那台电脑是你用的,另外一台电脑是他用的……我的、你的、他的电脑看起来都一样,都有显示器、主机、键盘等等组成,我们都可以用它来工作、学习,这些同一种类型(type)的东西都叫电脑,这可以称为“电脑类”。类就是某种东西的类型。可以将类,看成是对象的蓝图。电脑类这个蓝图应该描述电脑的基本构成组件:显示器、键盘、鼠标、CPU、内存、主板、光驱等等,电脑类这个蓝图也应该描述它应该具备的基本功能:开机、关机、打字等等。换句话来说,类应该描述该类的对象所应包含的状态(state)信息和它们将能支持的行为(behavior)。
在面向对象的世界里,状态和行为结合成类的定义。这个定义用于作为创建对象的蓝图。电脑生产商根据电脑的规格描述生产电脑。也就是根据类创建对象。类的状态信息,也称为属性(attribute),比如:显示器、键盘、鼠标、CPU等等;类的行为称为操作(operation),比如:开机、关机、打字等等。
属性与操作,是描述一个类的最重要的内容。在JAVA中,操作也称为方法(method)。
那么我们需要定义一个什么样的类,或如何定义一个类呢?换句话来讲,就是我要定义的类,应该包括哪些属性和操作呢?这将涉及到一个概念:抽象
抽象
我们把抽象看成一个动词!所谓抽象,就是把现实世界中的事物用类来根据语境和需要进行描述。比如我的电脑CPU风扇运转不正常了,你的电脑光驱弹出有问题,这些内容,在电脑这个类中是否需要描述呢?对于生产电脑的系统来说,它可以不关心这些无需关心的细节;但是对于修理电脑的系统来说,这是必不可少的细节!这就是语境。
再比如说:我现在要开发一个学生信息管理系统,入学的时候,要把学生的信息录入,这时候,我关心学生的姓名、性别、专业、班级、民族、学生卡号等等,那么至于学生头发的颜色、穿什么衣服、有多少钱等等这些我们就不关心了。这就是关注该关注的,而忽略不重要的,无需关注的信息。
对现实进行抽象,这是我们在用面向对象的思维去开发软件的时候,要面对的一项真正的挑战!可以说,我们后面要开发的每一个项目,你都要进行相应的思维训练!训练你的抽象能力。
思维训练:
我手上拿着一根香烟:这根香烟就是“香烟”这个类的一个对象
一个班上有45个学生:那45个学生就是“学生”这个类的45个对象
大街上跑着3辆汽车:那3辆汽车就是“汽车”这个类的3个对象
继承
下面我们来学习面向对象三大特征(继承、封装、多态)之一的“继承”。世间万物皆对象,我们需要对这些对象进行归类,在归类过程中,我们发现有很多类型之间具有相同的特征。比如你家里养了一只猫,你家的那只猫可以称为“猫”这个类的一个对象;我家养了一条狗,我家的这条狗是“狗”这个类的一个对象;他家养了一只鸟,他家的这只鸟是“鸟”这个类的一个对象……那么,我们发现,猫、狗、鸟,都可以用同一个名称“动物”来称呼它!换句话来说,猫、狗、鸟都是一种动物!用面向对象的话来说,猫、狗、鸟这三个类“继承”了“动物”这个类。“动物”在这个关系中称为“父类/超类(super class)/基类”;猫、狗、鸟这三个类称为“子类(sub class)”。
在比如,上面说的电脑,台式机和笔记本都是电脑,笔记本如果按照厂商来分又可分IBM笔记本、HP笔记本、戴尔笔记本等等,当然,对于台式机也一样可以按照这种方式来划分,要不要这样划分,要看实际需要的情况。所以,IBM笔记本、戴尔笔记本是笔记本这个父类的子类,笔记本、台式机又是电脑这个父类的子类。
继承这个概念的目的之一是为了减少类定义的重复。电脑有显示器、键盘、鼠标、CPU、硬盘、内存这些属性和开机、关机、打字这些操作;那么,如果笔记本和台式机是电脑的子类,我们在定义笔记本和台式机的时候,就无需重复定义这些属性和操作了。
头脑风暴-思维训练:
请举出更多现实生活中存在继承关系的例子!
封装
封装(encapsulation)也是面向对象三大特征之一。这是面向对象中最重要的内容。对于台式机来讲,用一个机箱将主板、CPU、硬盘、内存这些零件包装(封装)起来,然后将鼠标、键盘、显示器这些东西开放出来让外界的人去操作。这就是关于封装的概念。电脑把一些重要的组件封装起来,避免外界的人对这些组件误操作而导致电脑异常(比如不小心把硬盘弄掉了);同时,电脑也需要暴露一些接口给外界操作。通过封装/隐藏重要的细节,同时暴露给外界一些操作的接口,外界的事物便能够和谐地和这些对象共处!
那么,接下来的问题就是:我们如何决定一个类该隐藏哪些属性或操作,该暴露哪些属性和操作呢?这些问题,就是面向对象的分析和设计中该回答的问题!需要根据具体情况来决定。我们后面的很多项目,也是要训练大家的这种思维的!
可见性
还是拿电脑来举例。比如我现在要用电脑的光驱来读一些光盘。我们通过电脑暴露给我们的接口(一个按键),我们按一下光驱的弹出按键,电脑内部就进行了一系列的动作,驱动电机的运转,把光驱弹出。这个弹出的过程,我根本不关心它是怎么弹出的,也不想关心它。所以,电脑就不应该将这个弹出的细节暴露出来!如果暴露出来了,可能会导致更大的问题,比如手动转动一个电机,不小心用力过猛把它弄坏了!如果把细节暴露出来,也导致了我们使用电脑的复杂度!电机本身(电脑的属性)以及驱动电机运行(电脑的操作)这些东西对外界隐藏起来了,称为私有(private)属性和私有操作。电脑提供了一个我们可以执行的“弹出光驱”操作,这个操作称为公有(public)操作。当然,比如键盘、显示器这些电脑的部件也暴露给我们了,可以称它们为公有(public)属性。
private、public就是可见性。可见性还包括protected和package可见性。只有电脑自身可以对象硬盘、内存、光驱这些private部件进行操作,也就是对电脑自身可见。换句话来说,具有private可见性的属性和操作,只有在类内部才可以访问。而具有public可见性的属性和操作,在任何地方(也就是其它任何对象)都可以访问。在类中声明为protected的元素,它的可见性程度介于private和public之间,可以被类内部的其它方法或类的所有子类(包括子类的子类)的方法访问。而如果在一个类中声明一个元素为package可见性,那么它能够被类自身和这个类所在的包(package)中的其它类访问。
现在我们来看看如何创建一个类,并定义它的属性(在类的内部,方法的外部定义的变量)和方法:
public class Computer {
//私有属性 private String harddisk; //硬盘
//私有属性 private String memory; //内存
//公有属性 public String display; //显示器
//公有属性 public String keyboard; //键盘
//公有属性 public String mouse; //鼠标
//公有方法:弹出光驱 public void popupCDRom(){ //do something }
//公有方法:弹入光驱 public String injectCDRom(String cd){
//在类的内部,才可以访问类内部的私有方法 String content = readCDContent(cd); return content; }
//私有方法:读取光盘的内容 private String readCDContent(String cd){ //do something else return "光盘"+cd+"上的内容:Hello World!"; } } |
测试它的可见性:
public class ComputerTest {
public static void main(String[] args){ Computer computer1 = new Computer();
//在ComputerTest这个类中,可以访问Computer中的public属性 computer1.display = "液晶显示器";
//在ComputerTest这个类中,可以访问Computer中的public方法 computer1.popupCDRom(); String content = computer1.injectCDRom("《2012》"); System.out.println(content);
//下面这行代码将导致编译错误 computer1.memory = "521M内存";
//下面这行代码将导致编译错误 computer1.readCDContent("《2012》");
//只有把ComputerTest这个类和Computer类放在一个package下面,才可以访问 computer1.otherProperties = "其它属性"; } } |
public class Person { private String name; protected int age; String address; }
public class Teacher extends Person { public void myNameIs(){ //编译错误 System.out.println(name); } public void myAgeIs(){ //age是protected,可以访问 System.out.println(age); } }
public class FemaleTeacher extends Teacher{ public void myAge(){
//age是protected,可以访问 System.out.println(age); } } |
上面的name/address/age,是定义在类的内部,方法体的外部的属性,称为成员变量,也称为实例变量(instance variable)或对象的状态(state)或字段(field)。
如何调用一个方法?参数必须匹配!
思考:如果在一个类中声明一个元素为package可见性,那么它是否能够被这个类的子类访问?
其它术语
l 一个类的对象一般也称为类的实例(instance)
l 类的属性(attribute)和操作(方法,method)也称为类的成员。
l 总结起来,定义在类内部,方法外部的属性,可称为instance variable(实例变量)或attribute(属性或特性)或member variable(成员变量)或field(字段),实例变量和成员变量这两个术语比较常用。
l 当说到属性这个词的时候,要特别注意:我们通常混淆了attribute和property之间的区别,如果属性指的是property,那么它的意思是由getters/setters方法所确定的property。在类内部直接定义实例变量/成员变量/字段称为attribute(请参考下面说的普遍被接受的基本原则)
l 为什么使用get/set?主要的目的还是为了封装(隐藏具体的实现细节),举例:
public class MyDateNoWrap { public int year; public int month; public int day; } |
public class MyDateNoWrapClient {
/** * @param args */ public static void main(String[] args) { MyDateNoWrap mdnw = new MyDateNoWrap();
//把内部结构暴露给了外界,在赋值时,数据无法得到有效控制 mdnw.year = 2009; mdnw.month = 15; //?? 导致类内部的数据不受控制! mdnw.day = 1;
//把内部结构暴露给了外界,在取值时,给客户端制造麻烦 //客户端必需知道这个结构,才能得到正确的值 String theday = mdnw.year + "-" + mdnw.month + "-" + mdnw.day; System.out.println(theday); }
} |
package cn.com.leadfar;
public class MyDate { private int year; private int month; private int day;
public String getTheDay(){ return year + "-" + month + "-" + day; }
public int getYear() { return year; } public void setYear(int year) { this.year = year; } public int getMonth() { return month; } public void setMonth(int month) {
if(month < 1 || month > 12){ throw new RuntimeException("您设置的月份数据有误!"); }
this.month = month; } public int getDay() { return day; } public void setDay(int day) {
if(month <1 || month > 31){ throw new RuntimeException("您设置的日期数据有误!"); }
this.day = day; } }
|
package cn.com.leadfar;
public class MyDateClient { public static void main(String[] args) { MyDate md = new MyDate(); md.setYear(2009); md.setMonth(12); md.setDay(20);
System.out.println(md.getTheDay()); } }
|
普遍被接受的基本原则
1、 属性应该被定义为private,除非在类继承结构中你需要把一个属性开放给你的子类使用(这时使用protected)。极少极少的情况,才需要定义为public。
2、 如果要将一个类内部的私有属性暴露给外界使用,通常不是将其定义为public,而是在类中针对这个属性创建get和set方法。get和set方法后面紧跟属性的名称,且第一个字母大写。
public class Person { private String name; private int age; private String address;
public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } } |
上述类定义,定义了name/age/address这三个实例变量,同时,这个类,也定义了name/age/address这三个property
3、如果在一个类中,定义了get或set方法,那么通常称它为属性(property),这区别于类中的成员变量的名称。
public class Person { private String _name; private int _age; private String _address;
public String getName() { return _name; } public void setName(String name) { this._name = name; } public int getAge() { return _age; } public void setAge(int age) { this._age = age; } public String getAddress() { return _address; } public void setAddress(String address) { this._address = address; } } |
比如上面这段代码,_name、_age、_address称为Person这个类的成员变量(或实例变量或字段或attribute),而name、age、address称为Person这个类的property。大家看:
getName()/setName这两个方法,以get/set开头,紧跟Name这个名称,N大写,这是习惯写法,而且大家也应该这样写(N这个字母不要写小写!),这种写法,就确定了名为name(注意是n而不是N)的property。
这些get/set方法,称为getters/setters。
如果一个property有get方法,称这个property是readable(可读的)的
如果一个property有set方法,称这个property是writable或modifiable(可写的)的
如果一个property只有get方法,称这个propety是readOnly(只读)
如果一个property只有set方法,称这个property是writeOnly(只写)