Java对象导论

抽象过程

所有的编程对象都提供抽象机制,解决问题的复杂程度取决于抽象的类型和对象,“类型”是指所抽象的是什么,将问题空间中的元素及其在解空间中的表示称为对象:代码可以通过添加新类型的对象使自身适用于某个特定问题。面向对象语言的五个基本特征:

  1. 万物皆为对象。
  2. 程序是对象的集合,他们通过发送消息来告知彼此要做的。
  3. 每个对象都有自己的由其他对象所构成的存储。(可以通过创建现有对象的包的方式来创建新的类型的对象)
  4. 每个对象都拥有其类型。(每个对象都是某个类的一个实例)
  5. 某一特定类型的对象都可以接收同样的信息。

每个对象都有一个接口,因为类描述了具有相同特征和行为的对象集合,所以一个类实际上就是一个数据类型,一旦类被创建,就可以创建类的无数个对象,然后去操作他们。

每个对象都提供服务。


继承

继承要解决的问题是:当创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。

类型不仅仅只是描述了作用于一个对象集合上的约束条件,同时还有与其他类型之间的关系。

两个类型可以有相同的特性和行为,但是其中一个类型可能比另一个含有更多的特性,并且可以处理更多的消息。

继承使用基类型和导出类型的概念表示了这种类型之间的相似性。

一个基类型包含其所有导出类型所共享的特性和行为。可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可能被实现的各种不同方式。

有两种方法可以使基类与导出类产生差异:

    1.直接在导出类中添加新方法。
    2.改变现有基类的方法的行为(覆盖 overriding)。

覆盖:我正在使用相同的接口方法,但是我想在新类型中做些不同的事情。
“是一个”与“像是一个”关系

如果导出类和基类是完全相同的类型,具有完全相同的接口。结果可以用一个导出类对象来完全替代一个基类对象。这可以被视为纯粹替代,通常称之为替代原则。

我们将这种情况下的基类与导出类之间的关系称为:is-a (是一个)关系。

有时必须在导出类型中添加新的接口元素,这样也就扩展了接口。这个新的类型仍然可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法。

我们将这种情况下的基类与导出类之间的关系称为:is-like-a (像是一个)关系。


容器

通常来说,如果不知道在解决某个特定问题时需要多少个对象,或者它们将存活多久,那么就不可能知道如何存储这些对象。如何才能知道需要多少空间来创建这些对象呢?答案是你不可能知道,因为这类信息只有在运行时才能获得。

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

为什么要涉及不同种类的序列呢?第一,不同容器提供了不同类型的接口和外部行为。堆栈相比于队列就具备不同的接口和行为,也不同于集合和列表的接口和行为。它们之中的某种容器提供的解决方案可能比其他容器要灵活得多。第二,不同的容器对于某种操作具有不同的效率。最好的例子就是两种List的比较:ArrayList和LinkedList。它们都是具有相同接口和外部行为的简单的序列,但是它们对某些操作所花费的代价却有天壤之别。在ArrayList中,随机访问元素是一个花费固定时间操作;但是对LinkedList来说,随机选取元素需要在列表中移动,这种代价是高昂的,访问越靠近表尾的元素,花费的时间越长。而另一方面,如果想在序列中间插入一个元素,LinkedList的开销却比ArrayList要小。上述操作以及其他操作的效率,依序列底层结构的不同而存在很大的差异。我们可以在一开始使用LinkedList构建程序,而在优化系统性能时改用ArrayList。接口List所带来的抽象,把在容器之间进行转换时对代码产生的影响降到最小限度。

参数化类型

在Java 5出现之前,容器存储的对象都只具有Java中的通用类型:Object。

单根继承结构意味着的有东西都是Object类型,所以可以存储Object的容器可以存储任何东西。这使得容器很容易被复用。

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

所以,怎样才能将它变回先前置入容器中时的具有实用接口的对象呢?

这里再度用到了转型,但这一次不是向继承结构的上层转型为一个更泛化的类型,而是向下转型为更具体的类型。这种转型的方式称为向下转型。

我们知道,向上转型是安全的,但除非确切知道所要处理的对象的类型,否则向下转型几乎是不安全的。

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

向下转型和运行时的检查需要额外的程序运行时间,也需要程序员付出更多的心血。那么创建这样的容器,它知道自己所保存的对象的类型,从而不需要向下转型以及消除犯错误的可能,这样不是更有意义吗?

这种解决方案被称为参数化类型机制。参数化类型就是一个编译器可以自动定制于特定类型上的类。


泛型

Java 5的重大变化之一就是增加了参数化类型,在Java中它称为泛型。

一对尖括号,中间包含类型信息,通过这些特征就可以识别对泛型的使用。
 


对象的创建和生命期

在使用对象时,最关键的问题之一是它们的生成和销毁方式。每个对象为了生存都需要资源,尤其是内存。当我们不再需要一个对象时,它必须被清理掉,使其占有的资源可以被释放和重用。

对象的数据位于何处?怎样控制对象的生命周期?C++认为效率控制是最重要的议题,所以给程序员提供了选择的权力。为了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象置于堆栈(有时称为自动变量或限域变量)或静态存储区域内来实现。这种方式将存储空间分配和释放置于优先考虑的位置,某些情况下这样控制非常有价值。但是也牺牲了灵活性,因为必须在编写程序时知道对象确切的数量、生命周期和类型。如果试图解决更一般化的问题,例如计算机辅助设计、仓库管理或者空中交通控制,这种方式就显得过于受限了。

第二种方式是在称为堆(heap)的内存池中动态地创建对象。在这张方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型是什么。这些问题的答案只能在程序运行时相关代码被执行到的那一刻才能确定。如果需要一个新对象,可以在需要的时刻直接在堆中创建。因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储空间,这可能要远远大于在堆栈中创建存储空间的时间。在堆栈中创建存储空间和释放存储空间通常各需要一条汇编指令即可,分别对应将栈顶指针向下移动和将栈顶指针向上移动。创建堆存储空间的时间依赖于存储机制的设计。

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

对于允许在堆栈上创建对象的语言,编译器可以确定对象存活的时间,并可以自动销毁它。然而,如果是在堆上创建对象,编译器就会对它的生命周期一无所知。在像C++这样的语言中,必须通过编程方式来确定何时销毁对象,这可能会因为不能正确处理而导致内存泄漏。Java提供了被称为“垃圾回收器”的机制,它可以自动发现对象何时不再被使用。更重要的是,垃圾回收器提供了更高层的保障,可以避免暗藏的内存泄漏问题,这个问题已经使许多C++项目折戟沉沙。

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

异常处理:处理错误

大多数错误处理机制的主要问题在于,它们都依赖于程序员自身的警惕性,这种警惕性来源于一种共同的约定,而不是编程语言所强制的。如果程序员不够警惕——通常是因为太忙,这些机制就很容易被忽视。

异常处理将错误处理直接置于编程语言中,有时甚至置于操作系统中。异常是一种对象,它从出错地点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。异常处理就像是与程序正常执行路径并行的、在错误发生时执行的另一条路径。因为它是另一条完全分离的执行路径,所以它不会干扰正常的执行代码。这往往使得代码编写变得简单,因为不需要被迫定期检查错误。此外,被抛出的异常不像方法返回的错误值和方法设置的用来表示错误条件的标志位那样可以被忽略。异常不能被忽略,所以它保证一定会在某处得到处理。最后需要指出的是:异常提供了一种从错误状况进行可靠恢复的途径。现在不再只是退出程序,你可以经常进行校正,并恢复程序的执行,这些都有助于编写出更健壮的程序。

Java的异常处理在众多的编程语言中格外引人注目,因为Java一开始就内置了异常处理,而且强制你必需使用它。它是唯一可接受的错误报告方式。如果没有编写正确的处理异常的代码,那么就会得到一条编译时的出错消息。这种有保障的一致性有时会使得错误处理非常容易。

值得注意的是,异常处理不是面向对象的特征——尽管在面向对象语言中异常常常被表示成为一个对象。异常处理在面向对象语言出现之前就已经存在了。

并发编程

在计算机编程中有一个基本概念,就是在同一时刻处理多个任务的思想。最初,程序员用所掌握的有关机器底层的知识来编写中断服务程序,主进程的挂起是通过硬件中断来触发的。尽管这么做可以解决问题,但是其难度太大,而且不能移植,所以使得将程序移植到新型号的机器上时,既费时又费力。

有时中断对于处理时间飨强的任务是必需的,但是对于大量的其他问题,我们只是想把问题切分成多个可独立运行的部分(任务),从而提高程序的响应能力。在程序中,这些彼此独立运行的部分称之为线程,上述概念被称为“并发”。并发最常见的例子就是用户界面。通过使用任务,用户可以在按下按钮后快速得到一个响应,而不用被迫等待到程序完成当前任务为止。

通常,线程只是一种为单一处理器分配执行时间的手段。但是如果操作系统支持多处理器,那么每个任务都可以被指派给不同的处理器,并且它们是在真正地并行执行。在语言级别上,多线程所带来的便利之一便是程序员不用再操心机器上是有多个处理器还是只有一个处理器。由于程序在逻辑上被分为线程,所以如果机器拥有多个处理器,那么程序不需要特殊调整也能执行得更快。

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

Java的并发是内置于语言中的,Java SE5已经增添了大量额外的库支持。
 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值