《Java编程思想》第一章:对象导论

对象导论

抽象过程

面向对象程序设计方式的特性:

  • 万物皆为对象
  • 程序是对象的集合,它们通过发送消息来告知彼此所要做的
  • 每个对象都有自己的由其他对象所构成的存储
  • 每个对象都拥有类型
  • 某一特定类型的所有对象都可以接收同样的消息
	对象具有状态、行为和标识。这意味着每一个对象都可以拥有内部数据(它们给出了该对象的状态)和方法(它们产生行为),并且每一个对象都可以唯一地与其他对象区分开来,具体说来,就是每一个对象在内存中都有一个唯一的地址。

每个对象都有一个接口

​ 所有的对象都是唯一的,具有相同的特性和行为的对象是所归属的类的一部分。

​ 你可以创建某一类型的变量(对象或者实例),然后操作这些变量(称为发送消息或者请求,发送消息,对象就知道要做什么)。每个类的成员或元素都具有某种共性,每一个对象都属于定义了特性和行为的某个特定的类。

​ 因为类描述了具有相同特性(数据元素)和行为(功能)的对象集合,所以一个类实际上就是一个数据类型。

​ 怎样才能获得有用的对象?

	必须有某种方式产生对对象的请求,使对象完成各种任务,如完成一笔交易、在屏幕上画图、打开开关等等。每个对象都只能满足某些请求,这些请求由对象的接ロ( interface)所定义,决定接口的便是类型。

​ 接口确定了对某一特定对象所能发出的请求。但是,在程序中必须有满足这些请求的代码。这些代码与隐藏的数据一起构成了实现。从过程型编程的观点来看,这并不太复杂。在类型中,每一个可能的请求都有一个方法与之相关联,当向対象发送请求时,与之相关联的方法就会被调用。此过程通常被概括为:向某个对象“发送消息”(产生请求),这个对象便知道此消息的目的,然后执行对应的程序代码。

每个对象都提供服务

​ 当正在试图开发或理解一个程序设计时,最好的方法之一就是将对象想像为“服务提供者"。程序本身将向用户提供服务,它将通过调用其他对象提供的服务来实现这一目的。你的目标就是去创建(或者最好是在现有代码库中寻找)能够提供理想的服务来解决问题的一系列对象。

被隐藏的具体实现

​ 访问控制的第一个存在原因就是让客户端程序员无法触及他们不应该触及的部分,这些部分对数据类型的内部操作来说是必需的,但并不是用户解决特定问题所需的接口的一部分。这对客户端程序员来说其实是一项服务,因为他们可以很容易地看出哪些东西对他们来说很重要,而哪些东西可以忽略。

​ 访问控制的第二个存在原因就是允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。例如,你可能为了减轻开发任务而以某种简单的方式实现了某个特定类,但稍后发现你必须改写它才能使其运行得更快。如果接口和实现可以清晰地分离并得以保护那么你就可以轻而易举地完成这项工作。

​ Java用三个关键字在类的内部设定边界:public、private、protected。这些访问指定词决定了紧跟其后被定义的东西可以被谁使用。public表示紧随其后的元素对任何人都是可用的,而 private这个关键字表示除类型创建者和类型的内部方法之外的任何人都不能访问的元素。 private就像你与客户端程序员之间的一堵砖墙,如果有人试图访问 private成员,就会在编译时得到错误信息。 protected关键字与 private作用相当,差别仅在于继承的类可以访问 protected成员,但是不能访问 private成员。

​ Java还有一种默认的访问权限——default,当没有使用前面提到的任何访问指定词时,它将发挥作用。这种权限通常被称为包访问权限,因为在这种权限下,类可以访问在同一个包(库构件)中的其他类的成员,但是在包之外,这些成员如同指定了 private一样。

复用具体实现

​ 最简单地复用某个类的方式就是直接使用该类的一个对象,此外也可以将那个类的一个对象置于某个新的类中。我们称其为“创建一个成员对象”。新的类可以由任意数量、任意类型的其他对象以任意可以实现新的类中想要的功能的方式所组成。因为是在使用现有的类合成新的类,所以这种概念被称为组合( composition),如果组合是动态发生的,那么它通常被称为聚合( aggregation)。组合经常被视为“has-a”(拥有)关系,就像我们常说的“汽车拥有引擎”一样。

image-20210807170403013

继承

​ 在创建一个类之后,即使另一个新类与其具有相似的功能,我们还是得重新创建一个新类。如果我们能够以现有的类为基础,复制它,然后通过添加和修改这个副本来创建新类那就要好多了。通过继承便可以达到这样的效果,不过也有例外,当源类(被称为基类、超类或父类)发生变动时,被修改的“副本”(被称为导出类、继承类或子类)也会反映出这些变动。

image-20210807170414363

​ 当继承现有类型时,也就创造了新的类型。这个新的类型不仅包括现有类型的所有成员(尽管 private成员被隐藏了起来,并且不可访问),而且更重要的是它复制了基类的接口。也就是说,所有可以发送给基类对象的消息同时也可以发送给导出类对象。由于通过发送给类的消息的类型可知类的类型,所以这也就意味着导出类与基类具有相同的类型。在前面的例子中,“一个圆形也就是一个几何形”。通过继承而产生的类型等价性是理解面向对象程序设计方法内涵的重要门。

​ 有两种方法可以使基类与导出类产生差异。第一种方法非常直接:直接在导出类中添加新方法。这些新方法并不是基类接口的一部分。这意味着基类不能直接满足你的所有需求,因此必需添加更多的方法。这种对继承简单而基本的使用方式,有时对问题来说确实是一种完美的解决方式。但是,应该仔细考虑是否存在基类也需要这些额外方法的可能性。这种设计的发现与迭代过程在面向对象程序设计中会经常发生(如图所示)。

image-20210807170445048

​ 虽然继承有时可能意味着在接口中添加新方法(尤其是在以 extends关键字表示继承的Java中),但并非总需如此。第二种也是更重要的一种使导出类和基类之间产生差异的方法是改变现有基类的方法的行为,这被称之为覆盖( overriding)那个方法(如图所示)。

image-20210807170456833

"is-a"和"is-like-a"的关系

​ 对于继承可能会引发某种争论:继承应该只覆盖基类的方法(而并不添加在基类中没有的新方法)吗?如果这样做,就意味着导出类和基类是完全相同的类型,因为它们具有完全相同的接口。结果可以用一个导出类对象来完全替代一个基类对象。这可以被视为免粹替代,通常称之为替代原则。在某种意义上,这是一种处理继承的理想方式。我们经常将这种情况下的基类与导出类之间的关系称为**“is-a”(是一个)关系,因为可以说“一个圆形就是一个几何形状判断是否继承,就是要确定是否可以用is-a**来描述类之间的关系,井使之具有实际意义。

​ 有时必须在导出类型中添加新的接口元素,这样也就扩展了接口。这个新的类型仍然可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法。这种情况我们可以描述为**“is-ike-a”**(像是一个)关系。新类型具有旧类型的接口,但是它还包含其他方法,所以不能说它们完全相同。

image-20210807170503944

​ 当然,在看过这个设计之后,很显然会发现,制冷系统这个基类不够一般化,应该将其更名为“温度控制系统”,使其可以包括制热功能,这样我们就可以套用替代原则了。这张图说明了在真实世界中进行设计时可能会发生的事情。

​ 当你看到替代原则时,很容易会认为这种方式(纯粹替代)是唯一可行的方式,而且事实上,用这种方式设计是很好的。但是你会时常发现,同样显然的是你必须在导出类的接口中添加新方法。只要仔细审视,两种方法的使用场合应该是相当明显的。

伴随多态的可互换对象

​ 我们需要编写一段代码,仅仅只和基类进行交互,这段代码和具体类型信息是分离的,这样做使代码编写更为简洁、理解。

void doSomething(Shape shape){
    shape.erase();
    //.....
    shape.draw();
}

这个方法可以和任何Shape进行对话,因此它是独立于任何它要绘制和擦除的对象的具体类型的。如果程序中其他部分用到了**doSomething()**方法:

Circle circle = new Circle();  // Circle是Shape的子类
Line line = new Line(); // Line是Shape的子类
doSomething(circle);
doSomething(line);

​ 对doSomething()的调用会自动地正确处理,而不管对象的确切类型。

​ 当Circle被传入到预期接收Shape的方法中,究竟会发生什么。由于 Circle可以被 dosomething()看作是 Shape,也就是说, dosomething()可以发送给 Shape的任何消息, Circle都可以接收,那么,这么做是完全安全且合乎逻辑的。

dosomething()的代码给人印象深刻之处在于,不知何故,它总是做了该做的。调用 Circledraw()方法所执行的代码与调用 SquareLinedraw()方法所执行的代码是不同的,而且当draw()消息被发送给一个匿名的 Shape时,也会基于该 Shape的实际类型产生正确的行为。

​ 这相当神奇,因为就像在前面提到的,当Java编译器在编译 dosomething()的代码时,并不能确切知道 dosomething()要处理的确切类型。所以通常会期望它的编译结果是调用基类 Shapeerase()draw()版本,而不是具体的 CircleSquareLine的相应版本。正是因为多态才使得事情总是能够被正确处理。编译器和运行系统会处理相关的细节,你需要马上知道的只是事情会发生,更重要的是怎样通过它来设计。当向一个对象发送消息时,即使涉及向上转型(把导出的类看做是它的基类的过程称为向上转型),该对象也知道要执行什么样的正确行为。

单根继承结构

​ 在OOP中,自C++面世以来就已变得非常瞩目的一个问题就是,是否所有的类最终都继承自单一的基类? 在Java中(事实上还包括除C++以外的所有OOP语言),答案是yes,这个终极基类的名字就是 Object。事实证明,单根继承结构带来了很多好处。

​ 在单根继承结构中的所有对象都具有一个共用接口,所以它们归根到底都是相同基本类型。

​ 单根继承结构保证所有对象都具备某些功能。

​ 单根继承结构使垃圾回收器的实现变得容易很多。由于所有对象都保证具有其类型信息,因此不会因无法确定对象的类型而陷入僵局。这对于系统级操作(如异常处理)显得尤其重要,并且给编程带来了更大的灵活性。

容器

​ 在C++中,容器是标准C++类库的一部分,经常被称为标准模板类库( Standard Template Library,STL). Object Pascal在其可视化构件库( Visual Component Library,VCL)中有容器; Smalltalk提供了一个非常完备的容器集;Java在其标准类库中也包含有大量的容器。在某些类库中,一两个通用容器足够满足所有的需要;但是在其他类库(例如Java)中,具有满足不同需要的各种类型的容器,例如List(用于存储序列),Map(也被称为关联数组,用来建立对象之间的关联),Set(每种对象类型只持有一个),以及诸如队列、树、堆栈等更多的构件。

参数化类型

​ 在 Java SE5出现之前,容器存储的对象都只具有Java中的通用类型: Object单根继承结构意味着所有东西都是 Object类型,所以可以存储 Objects的容器可以存储任何东西。这使得容器很容易被复用。

​ 要使用这样的容器,只需在其中置入对象引用,稍后还可以将它们取回。但是由于容器只存储 Object,所以当将对象引用置入容器时,它必须被向上转型为 Object,因此它会丢失其身份。当把它取回时,就获取了一个对 Object对象的引用,而不是对置入时的那个类型的对象的引用。所以,怎样才能将它变回先前置入容器中时的具有实用接口的对象呢?

​ 这里再度用到了转型,但这一次不是向继承结构的上层转型为一个更泛化的类型,而是向下转型为更具体的类型。这种转型的方式称为向下转型。我们知道,向上转型是安全的,例如Circle是一种 Shape类型,但是不知道某个 ObjectCircle还是 Shape,所以除非确切知道所要处理的对象的类型,否则向下转型几乎是不安全的。

​ 然而向下转型并非彻底是危险的,因为如果向下转型为错误的类型,就会得到被称为异常的运行时错误。尽管如此,当从容器中取出对象引用时,还是必须要以某种方式记住这些对象究竟是什么类型,这样才能执行正确的向下转型。

Java SE5的重大变化之一就是增加了参数化类型,在Java中它称为范型。一对尖括号,中间包含类型信息,通过这些特征就可以识别对范型的使用。例如,可以用下面这样的语句来创建一个存储 ShapeArraylist

ArrayList<Shape> shapes = new ArrayList<Shape>();

对象的创建和生命周期

Java完全采用了动态内存分配方式,每当想要创建新对象时,就要使用new关键字来构建此对象的动态实例。

Java提供了“垃圾回收器”的机制,它可以自动发现某个对象何时不再被使用,并继而销毁它。

Java的垃圾回收器被设计用来处理内存释放问题(尽管它不包括清理对象的其他方面)。垃圾回收器“知道”对象何时不再被使用,并自动释放对象占用的内存。这一点同所有对象都是继承自单根基类 Object以及只能以一种方式创建对象(在堆上创建)这两个特性结合起来,使得用Java编程的过程较之用C++编程要简单得多,所要做出的决策和要克服的障碍也要少得多。

异常处理:处理错误

​ 异常处理将错误处理直接置于编程语言中,有时甚至置于操作系统中。异常是一种对象,它从出错地点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。异常处理就像是与程序正常执行路径并行的、在错误发生时执行的另一条路径。

并发编程

​ 多线程有一个隐患:共享资源。如果有多个并行任务都要访问同一项资源,那么就会出向题。例如,两个进程不能同时向一台打印机发送信息。为了解决这个问题,可以共享的资源,例如打印机,必须在使用期间被锁定。因此,整个过程是:某个任务锁定某项资源,完成其任务,然后释放资源锁,使其他任务可以使用这项资源。

Java与Internet

web是什么

​ 客户/服务器系统的核心思想是:系统具有一个中央信息存储池( central repository of information),用来存储某种数据,它通常存在于数据库中,你可以根据需要将它分发给某些人员或机器集群。

​ 客户/服务器概念的关键在于信息存储池的位置集中于中央,这使得它可以被修改,并且这些修改将被传播给信息消费者。总之,信息存储池、用于分发信息的软件以及信息与软件所驻留的机器或机群被总称为服务器。驻留在用户机器上的软件与服务器进行通信,以获取信息、处理信息,然后将它们显示在被称为客户机的用户机器上。

web就是一台巨型服务器

​ Web实际上就是一个巨型客户服务器系统,但稍微差一点,因为所有的服务器和客户机都同时共存于同一个网络中。你不需要了解这些,因为你所要关心的只是在某一时刻怎样连接到台服务器上,并与之进行交互(即便你可能要满世界地査找你想要的服务器)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值