对象导论
1.抽象 所有的编程语言都提供抽象机制。例如汇编对机器进行了轻微的抽象,C语言对汇编进行了抽象。相比较而言,这些语言对于解决问题都有较强的局限性。因为它们会强迫你按照“机器”的角度进行思考。程序员必须根据机器模型和实际问题模型来建立关联。这对于开发来讲是非常不变的。正是因为如此,面向对象的思想应运而生。
在面向对象的世界里,所有的问题都可以看成是对象。程序员不必再受限于待解决的特定类型的问题。我们可以将问题空间中的元素和解空间中的表示称之为对象。这句话读起来非常拗口,不过我们只需要记住,把任何问题都看做对象,更适合描述我们现实中的世界,与我们人类的思维也更为接近。
纯粹的面向对象程序设计方式:
- 万物皆为对象。可以将现实世界,要解决的问题,等一切概念化的构件抽象为对象。
- 程序是对象的集合,它们通过发送消息来告知彼此要做的事情。这句话对于初学者来说可能会有一些歧义,要时刻记住的是,代码定义的是类型,程序中活跃的永远是对象。调用对象中的某个方法相当于向这个对象“发送了消息”,由这个对象根据消息决定要做什么事。
- 每个对象都有自己的由其他对象所构成的存储。通俗一点理解就是对象可以包含其他的对象。正是由于这样的特性我们才可以基于面向对象构建复杂的架构,将复杂性隐藏与对象的简单性之后。
- 每个对象都拥有其类型。这个理解起来很简单,对象是程序中活跃的元素,每个对象都是它类型的一个特殊的实例。比如,人是一种类型,张三是这个类型的一个对象,李四是这个类型中的另一个对象,张三和李四是不同的实体,但是他们同属于“人”这个类型。
- 某一个特定类型的对象可以接受同样的消息。人都可以走,鸟都可以飞。更有深意的理解是,“圆”类型的对象,一定是一个“几何体”类型的对象,所以圆形类型的对象一定能接受发给几何形类型对象的消息。所以编写代码时候尽量编写与“更基本的类型”交互的代码。因为子类型的同样适用于这些代码。这种“可替代性”也就是java中向上转型的思想体现。
总结:对象具有状态(java中用数据成员表示), 行为(java中用方法成员表示), 标识,(每个对象都可以和其他的对象完全区分开来)。
2. 接口与服务。
每个对象都需要对外暴露接口,声明它所能提供的服务。这里的接口和java中的关键字interface不是一个概念,这里的接口相当于对外界的通道,具体来讲就是类中声明的方法。
1 public class Light { 2 3 private int brightness; 4 5 public void on(){ 6 System.out.println("the light is on"); 7 } 8 9 public void off(){ 10 System.out.println("the light is off"); 11 } 12 13 public static void main(String[] args){ 14 Light light = new Light(); 15 light.on(); 16 light.off(); 17 } 18 }
这是一个简单的示例。Light定义了一个灯类型,这个类型的所以对象看起来都应该具有这样的属性,“brightness”,这个类型的所有对象都应该具有这样两个接口, “on”, "off",在main函数中(程序的入口)创建了Light类型的一个对象,对象的名字叫light,
可以向这个对象发送消息,通过调用“on” 和 “off” 方法,让它打开或关闭。这里我调用了系统类打印字符串来模拟打开和关闭。这里没有用到属性,只是为了展示“对象的接口”的含义。
这个示例向我们展示了这样一个理念,在写代码的同时要时刻思考,我创造的类型能够抽象实际中的什么问题,我的类型的对象能向外界提供什么样的服务(通过接口),我应该调用哪些类去提供这样的服务(类的组合或继承),想明白问题就可以迎刃而解。
3.访问控制机制
有些时候,对类里面的成员的访问加以控制是必要的。可以将程序开发人员分为两类,一类是类的创建者,即创建这个类的人,还有一类是客户端程序员,即使用这些类的人。一般来讲,类的创建者只需要暴露给用户那些希望用户可以使用的接口,而隐藏其他需要保护的部分,一是因为这样的机制可以保护程序不被粗心的程序员滥用和破坏,二是允许类的设计者可以改变类的内部实现方式而不会影响到其他使用这个类的程序员。这是使用访问控制机制的根本原因。
java中使用三个关键字,public, private, protected设定边界。可以先记住,public修饰的成员在任何地方都可用,private修饰的成员只能在这个类内部被使用,protected修饰的成员可以在类内部和继承这个类的内部被访问,同时protected具有默认的包访问权限。没有被任何关键字修饰的成员同样具有默认的包访问权限。
4.继承还是组合
代码复用是面向对象设计语言最大的优点之一。最简单的就是创建一个类型的对象,将它放置与另一个类中,称之为“创建一个成员对象”,这样的复用方式具有很大的灵活性。例如,“船”这个类型,需要有“浆”这个类型的对象作为其数据成员,可以创建一个这样的对象将其放入“船”类型中。
继承的适用场景:如果能够以现有的类为基础,通过添加或者修改这个类的接口来创建我们所需的类型。继承类会包含基类的所有成员,无论是public还是private成员,不仅如此它还复制了基类的所有接口。也就是说所有发送给基类的消息同样可以发送给导出类。
继承现有的类型的时候,创造了一个新的类型,这个类型不仅包括现有类型的所有成员,(包括private成员),更重要的是继承类复制了基类的接口。也就是说所有发送给基类对象的消息也同样可以发送给导出类,这就意味者基类与导出类具有相同的类型。
如上图展示的这样,所有的图形,圆形,方形,三角形都继承了基类的基础接口。在程序中可以随时添加新的导出类,基类包含所有导出类具有的公共特性。当然,由于基类与导出类具有相同的基础接口,如果导出类只是简单的继承基类,而不去做其他任何事情,那么导出类不仅与基类拥有相同的类型,还具有相同的行为,显然这样做没有任何意义。
因此,使基类与导出类产生差异的的方法有两种,一是覆盖基类中的接口,二是在导出类中添加新的接口。
5.是一个,还是像一个
简单来说,如何继承只是覆盖基类的方法不在导出类中添加任何新的方法,那么基类与导出类就具有完全相同的接口,可以用一个导出类的对象来代替基类的对象。这就是“是一个”的关系,例如,我们可以说,圆形是一个图形。如果在导出类中添加了新的方法,那么这就是“像一个”的关系。
6.多态与向上转型
先写一个简单的示例。
public class Bird { public void fly(){ } }
public class Goose extends Bird { @Override public void fly(){ System.out.println("Goose" + " " + "fly"); } }
public class Penguin extends Bird{ @Override public void fly(){ System.out.println("Penguin" + " " + "fly"); } }
这里定义了一个“Bird”基类,“Goose”类和“Penguin”类分别继承了这个基类,并且重写了基类中的fly()接口。可以在想要重写的方法上加上@Override注解以检验是否正确覆盖了基类中的方法。在两个导出类中可以看出,它们分别实现了不同的“fly”方式。
public class BirdController { public void controller(Bird bird){ bird.fly(); } public static void main(String[] args){ BirdController birdController = new BirdController(); Bird bird1 = new Goose(); Bird bird2 = new Penguin(); birdController.controller(bird1); birdController.controller(bird2); } }
定义一个鸟类“控制器”,这个类中有个controller方法,只和“Bird”类型打交道。这看上去很奇怪,不过这正是“多态”思想的精髓。即“只编写与通用的基类打交道”的代码。可以看到,在main函数中创建了两个导出类对象,分别把它们赋给“基类”,这就是所说的向上转型。为什么叫做向上转型,就像前文中提到的UML类图中所示的那样,在图中基类画在导出类的上方,更惊奇的是编译器允许我们这样做,因为前文中已经表述过,基类和导出类具有相同的“类型”,可以把导出类看做是基类。
控制器的接口接受基类类型对象的参数,而导出类的对象正好满足条件。运行程序,
Goose fly Penguin fly Process finished with exit code 0
我们发现,导出类分别正确实现了各自的行为。因此也可以说BirdController适用于这个基类的不同导出类的状态,也就是实现了“多态”。
7.单根继承结构
有过编程经验的人都应该知道,C++中不是单根继承的,而java是。所谓单根集成就是每个类只允许继承一个类,而所有的类都直接或间接的继承自一个“超级基类” “Object”,这就是我们有时候在写程序时会发现,我们自己定义的类的对象在没有继承任何类的情况下仍然会有一些“多余的”方法可以调用,而这些方法在我们的类中没有定义,很明显,这些方法存在与“Object”中。
单根继承保证了我们的对象都具有一些“完全相同”的公共接口,换个角度就是我们可以把所以的对象都看做是“Object”类型的对象,这样的机制保证对象在堆中的创建和垃圾回收都变得很容易,而且在传递参数的时候也得到了极大的简化。比如我们可以编写一个使用Object类型对象作为参数的方法,很明显,这个方法可以接受任何类型的对象作为参数。
8.容器
容器是这样一种类型,它用于持有其他对象的引用,而这些对象的数量或类型在程序运行之前我们无法得知它们的数量或他们将存活多久。很明显,这种情况下数组的应用就有很大的局限性,因为在创建数组的同时我们必须确定数组的大小和数组所持有对象的具体类型。常用的容器,List, Set, Map。Map是一种非常强大的容器,它基于“键值”的形式存储对象,确切的说是对象的引用,程序员中有这样一句话,如果你只能用一种数据接口,那就用HashMap。
不同的容器提供了不同的接口和行为,不同的容器具有不同的运行效率,具体选择哪种容器需要根据程序的需要而定。
9.对象的创建和生命周期
程序中有一种非常宝贵的资源,那就是"内存"。程序是在内存中运行的,每个对象或基本数据类型的创建都要消耗内存。因此内存的回收是一个非常重要的问题。在简单的程序中这个问题看起来没什么挑战,创建对象,根据需要使用它,然后进行销毁,释放内存。当程序变得复杂时一切都不同了。你必须确定对象的生存周期,并且在不需要这个对象的时候释放掉。
针对这个问题,C++认为效率是第一位的,因此在C++中创建了一个对象后必须知道如何释放掉这个对象占用的资源。如果使用不当,很可能造成内存的泄露。
Java的对象在被称为”堆“的内存池中创建,这个内存池由jvm进行内存的管理,程序员只需要在需要对象的时候用new 关键字创建,其余的工作都交给jvm进行处理,网上有个大神的比喻我认为非常形象,C++的程序员都是大神,他们操纵着日月山河,(内存等资源)他们负责捕捉一个个试图逃逸的指针并将它们释放掉,而java程序员像一个长不大的孩子,他们在jvm这个完善的房子里尽情的玩耍,把房子弄的乱七八糟,整理房间的工作永远都是房子自己的事情。
10.异常处理机制
程序运行时难免会出现各种各样的错误和异常,而这些错误和异常一般都是靠程序员自身的”警觉“和不断的检查。一旦程序员出现疏忽很可能严重的错误就会被忽视。
java提供了相对完善的异常处理机制,在这种机制中异常处理的就像是另一条程序运行的路径,它与正常的程序运行路径一起并行,在错误发生时执行。因此不会干涉程序的正常运行,同时也使得程序变得更加简单,不用定期的检查各种可能出现的错误。
11.并发编程
一个线程就像一个执行流程,程序会沿着这个流程一直运行,直到被中断或者满足某些条件而退出。多线程可以让我们充分利用多核处理器的性能,编写更快的程序。java的并发是内置与语言内的,程序员只需要根据需要将程序的执行划分为多个逻辑线程而不必关心机器是否是多处理器的。