Java编程思想8——对象的容纳

为了满足常规编程的需要,我们要求能在任何时候、任何地点创建任意数量的对象。所以不可依赖一个已命名的句柄来容纳自己的每一个对象。

为解决这个非常关键的问题,Java提供了容纳对象(或者对象的句柄)的多种方式。其中内建的类型是数组。此外,Java的工具库提供了一些“集合类”(亦称作“容器类”)。利用这些集合类,我们可以容纳乃至操纵自己的对象。

1.数组

哪些地方使数组显得如此特别呢? 有两方面的问题将数组与其他集合类型区分开来:效率和类型。对于Java来说,为保存和访问一系列对象(实际是对象的句柄)数组,最有效的方法莫过于数组。

代价:创建一个数组对象时,它的大小是固定的,而且不可在那个数组对象的“存在时间”内发生改变。

在Java中,无论使用的是数组还是集合,都会进行范围检查——若超过边界,就会获得一个RuntimeException(运行期违例)错误。

它们将其当作Object类型处理(Object类型是Java中所有类的“根”类)。从某个角度看,这种处理方法是非常合理的:我们仅需构建一个集合,然后任何Java对象都可以进入那个集合(除基本数据类型外——可用Java的基本类型封装类将其作为常数置入集合,或者将其封装到自己的类内,作为可以变化的值使用)。这再一次反映了数组优于常规集合:创建一个数组时,可令其容纳一种特定的类型。这意味着可进行编译期类型检查,预防自己设置了错误的类型,或者错误指定了准备提取的类型。

考虑到执行效率和类型检查,应尽可能地采用数组。然而,当我们试图解决一个更常规的问题时,数组的局限也可能显得非常明显。

1.1 数组和第一类对象

无论使用的数组属于什么类型,数组标识符实际都是指向真实对象的一个句柄。那些对象本身是在内存“堆”里创建的。堆对象既可“隐式”创建(即默认产生),亦可“显式”创建(即明确指定,用一个new表达式)。堆对象的一部分(实际是我们能访问的唯一字段或方法)是只读的length(长度)成员,它告诉我们那个数组对象里最多能容纳多少元素。对于数组对象,“[]”语法是我们能采用的唯一另类访问方法。

1.1.1 基本数据类型集合

当然,假如准备一种基本数据类型,同时又想要集合的灵活性(在需要的时候可自动扩展,腾出更多的空间),就不宜使用数组,必须使用由封装的数据构成的一个集合。

1.2 数组的返回

返回的实际仍是指向数组的指针。但在Java里,我们永远不必担心那个数组的是否可用——只要需要,它就会自动存在。而且垃圾收集器会在我们完成后自动将其清除。

2.集合

Java提供了四种类型的“集合类”:Vector(矢量)、BitSet(位集)、Stack(堆栈)以及Hashtable(散列表)。

2.1 缺点:类型未知

使用Java集合的“缺点”是在将对象置入一个集合时丢失了类型信息。之所以会发生这种情况,是由于当初编写集合时,那个集合的程序员根本不知道用户到底想把什么类型置入集合。若指示某个集合只允许特定的类型,会妨碍它成为一个“常规用途”的工具,为用户带来麻烦。为解决这个问题,集合实际容纳的是类型为Object的一些对象的句柄。
当然,也要注意这并不包括基本数据类型,因为它们并不是从“任何东西”继承来的。这是一个很好的方案,只是不适用下述场合:

  • (1) 将一个对象句柄置入集合时,由于类型信息会被抛弃,所以任何类型的对象都可进入我们的集合——即便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把一条狗扔进来。
  • (2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的句柄。正式使用它之前,必须对其进行造型,使其具有正确的类型。

2.1.1 错误有时并不显露出来

2.1.2 生成能自动判别类型的Vector

新的GopherVector类有一个类型为Vector的private成员。它不会接收和产生普通
Object,只对Gopher对象感兴趣。

2.1.3 参数化类型

在C++中,它由语言通过“模板”获得了直接支持。

3.迭代器

它可以是一个对象,作用是遍历一系列对象,并选择那个序列中的每个对象,同时不让客户程序员知道或关注那个序列的基础结构。

4.集合的类型

4.1 Vector

public class CrashJava {
    @Override
    public String toString() {
        return "CrashJava address: " + this + "\n";
    }
    public static void main(String[] args) {
        CrashJava crashJava = new CrashJava();
        System.out.println(crashJava);
    }
}

toString()的+this会形成递归调用,因为this需要调用toString()转换成String。这就造成了在toString()里面调用toString,形成无限递归!

4.2 BitSet

4.3 Stack

4.4 Hashtable

对Java封装器能做的唯一事情就是将其初始化成一个特定的值,然后读取那个值。也就是说,一旦封装器对象已经创建,就没有办法改变一个值。

事实上是用Object的hashCode()方法生成每个对象的散列码,而且默认情况下只使用它的对象的地址。
同样地,默认的Object.equals()只是简单地比较对象地址。
因此,为了在散列表中将自己的类作为键使用,必须同时覆盖hashCode()和equals()。

许多Java库的类都覆盖了hashcode()和equals()方法,以便与自己提供的内容适应。

4.5 再论迭代器

5.排序

程序设计一个主要的目标就是“将发生变化的东西同保持不变的东西分隔开”。在这里,保持不变的代码是通用的排序算法,而每次使用时都要变化的是对象的实际比较方法。因此,我们不可将比较代码“硬编码”到多个不同的排序例程内,而是采用“回调”技术。利用回调,经常发生变化的那部分代码会封装到它自己的类内,而总是保持相同的代码则“回调”发生变化的代码。这样一来,不同的对象就可以表达不同的比较方式,同时向它们传递相同的排序代码。

6.通用集合库

7.新集合

集合库考虑到了“容纳自己对象”的问题,并将其分割成两个明确的概念:

  • (1) 集合(Collection):一组单独的元素,通常应用了某种规则。在这里,一个List(列表)必须按特定的顺序容纳元素,而一个Set(集)不可包含任何重复的元素。相反,“包”(Bag)的概念未在新的集合库中实现,因为“列表”已提供了类似的功能。
  • (2) 映射(Map):一系列“键-值”对(这已在散列表身上得到了充分的体现)。从表面看,这似乎应该成为一个“键-值”对的“集合”,但假若试图按那种方式实现它,就会发现实现过程相当笨拙。这进一步证明了应该分离成单独的概念。另一方面,可以方便地查看Map的某个部分。只需创建一个集合,然后用它表示那一部分即可。这样一来,Map就可以返回自己键的一个Set、一个包含自己值的List或者包含自己“键-值”对的一个List。和数组相似,Map可方便扩充到多个“维”,毋需涉及任何新概念。只需简单地在一个Map里包含其他Map(后者又可以包含更多的Map,以此类推)。

实际只有三个集合组件:Map,List和Set。而且每个组件实际只有两、三种实现方式。

在类的分级结构中,可看到大量以“Abstract”(抽象)开头的类,这刚开始可能会使人感觉迷惑。它们实际上是一些工具,用于“部分”实现一个特定的接口。举个例子来说,假如想生成自己的Set,就不是从Set接口开始,然后自行实现所有方法。相反,我们可以从AbstractSet继承,只需极少的工作即可得到自己的新类。尽管如此,新集合库仍然包含了足够的功能,可满足我们的几乎所有需求。所以考虑到我们的目的,可忽略所有以“Abstract”开头的类。

7.1 使用Collections

7.2 使用Lists

List(接口) 顺序是List最重要的特性;它可保证元素按照规定的顺序排列。List为Collection添加了大量方法,以便我们在List中部插入和删除元素(只推荐对LinkedList这样做)。List也会生成一个ListIterator(列表反复器),利用它可在一个列表里朝两个方向遍历,同时插入和删除位于列表中部的元素(同样地,只建议对LinkedList这样做)。

ArrayList 由一个数组后推得到的List。作为一个常规用途的对象容器使用,用于替换原先的Vector。允许我们快速访问元素,但在从列表中部插入和删除元素时,速度却嫌稍慢。一般只应该用ListIterator对一个ArrayList进行向前和向后遍历,不要用它删除和插入元素。

LinkedList 提供优化的顺序访问性能,同时可以高效率地在列表中部进行插入和删除操作。但在进行随机访问时,速度却相当慢,此时应换用ArrayList。也提供了addFirst(),addLast(),getFirst(),getLast(),removeFirst()以及removeLast()(未在任何接口或基础类中定义),以便将其作为一个规格、队列以及一个双向队列使用。

7.3 使用Sets

Set拥有与Collection完全相同的接口,所以和两种不同的List不同,它没有什么额外的功能。相反,Set完全就是一个Collection,只是具有不同的行为(这是实例和多态最理想的应用:用于表达不同的行为)。

Set(接口) 添加到Set的每个元素都必须是独一无二的;否则Set就不会添加重复的元素。添加到Set里的对象必须定义equals(),从而建立对象的唯一性。Set拥有与Collection完全相同的接口。一个Set不能保证自己可按任何特定的顺序维持自己的元素。

HashSet用于除非常小的以外的所有Set。对象也必须定义hashCode()。ArraySet 由一个数组后推得到的Set。面向非常小的Set设计,特别是那些需要频繁创建和删除的。对于小Set,与HashSet相比,ArraySet创建和反复所需付出的代价都要小得多。但随着Set的增大,它的性能也会大打折扣。不需要HashCode()的TreeSet 由一个“红黑树”后推得到的顺序Set。这样一来,我们就可以从一个Set里提到一个顺序集合。

7.4 使用Maps

Map(接口) 维持“键-值”对应关系(对),以便通过一个键查找相应的值。

HashMap 基于一个散列表实现(用它代替Hashtable)。针对“键-值”对的插入和检索,这种形式具有最稳定的性能。可通过构建器对这一性能进行调整,以便设置散列表的“能力”和“装载因子”。

ArrayMap 由一个ArrayList后推得到的Map。对反复的顺序提供了精确的控制。面向非常小的Map设计,特别是那些需要经常创建和删除的。对于非常小的Map,创建和反复所付出的代价要比HashMap低得多。但在Map变大以后,性能也会相应地大幅度降低。

TreeMap 在一个“红-黑”树的基础上实现。查看键或者“键-值”对时,它们会按固定的顺序排列(取决于Comparable或 Comparator)。TreeMap最大的好处就是我们得到的是已排好序的结果。TreeMap是含有subMap()方法的唯一一种Map,利用它可以返回树的一部分。

7.5 决定实施方案

实际上只有三个集合组件:Map,List和Set。而且每个接口只有两种或三种实施方案。

若需使用由一个特定的接口提供的功能,如何才能决定到底采取哪一种方案呢?

为理解这个问题,必须认识到每种不同的实施方案都有自己的特点、优点和缺点。

集合间的差异通常都可归纳为它们具体是由什么“后推”的。换言之,取决于物理意义上用于实施目标接口的数据结构是什么。

7.5.1 决定使用何种List

为体会各种List实施方案间的差异,最简便的方法就是进行一次性能测验。

ArrayList(以及Vector)是由一个数组后推得到的;而LinkedList是根据常规的双重链接列表方式实现的,因为每个单独的对象都包含了数据以及指向列表内前后元素的句柄。正是由于这个原因,假如想在一个列表中部进行大量插入和删除操作,那么LinkedList无疑是最恰当的选择(LinkedList还有一些额外的功能,建立于AbstractSequentialList中)。若非如此,就情愿选择ArrayList,它的速度可能要快一些。

7.5.2 决定使用何种Set

Set既可作为一个ArraySet实现,亦可作为HashSet实现。ArraySet是由一个ArrayList后推得到的,设计成只支持少量元素,特别适合要求创建和删除大量Set对象的场合使用。然而,一旦需要在自己的Set中容纳大量元素,ArraySet的性能就会大打折扣。写一个需要Set的程序时,应默认选择HashSet。而且只有在某些特殊情况下(对性能的提升有迫切的需求),才应切换到ArraySet。

进行add()以及contains()操作时,HashSet显然要比ArraySet出色得多,而且性能明显与元素的多寡关系不大。一般编写程序的时候,几乎永远用不着使用ArraySet。

7.5.3 决定使用何种Map

选择不同的Map实施方案时,注意Map的大小对于性能的影响是最大的。

当我们使用Map时,首要的选择应该是HashMap。只有在极少数情况下才需要考虑其他方法。

7.6 未支持的操作UnsupportedOperationException

7.7 排序和搜索

这些工具都属于两个新类的“静态”方法。这两个类分别是用于排序和搜索数组的Arrays,以及用于排序和搜索列表的Collections。

7.7.1 数组

Arrays类为所有基本数据类型的数组提供了一个重载的sort()和binarySearch(),它们亦可用于String和Object。

Comparable和Comparator
但假若我们不满足这一排序方式,又该如何处理呢?

针对Object数组(以及String,它当然属于Object的一种),可使用一个sort(),并令其接纳另一个参数:实现了Comparator接口(即“比较器”接口,新集合库的一部分)的一个对象,并用它的单个compare()方法进行比较。

Arrays类提供了另一个sort()方法,它会采用单个自变量:一个Object数组,但没有Comparator。这个sort()方法也必须用同样的方式来比较两个Object。通过实现Comparable接口,它采用了赋予一个类的“自然比较方法”。这个接口含有单独一个方法——compareTo(),能分别根据它小于、等于或者大于自变量而返回负数、零或者正数,从而实现对象的比较。

7.7.2 列表

可用与数组相同的形式排序和搜索一个列表(List)。用于排序和搜索列表的静态方法包含在类Collections中,但它们拥有与Arrays中差不多的签名:sort(List)用于对一个实现了Comparable的对象列表进行排序;binarySearch(List,Object)用于查找列表中的某个对象;sort(List,Comparator)利用一个“比较器”对一个列表进行排序;而binarySearch(List,Object,Comparator)则用于查找那个列表中的一个对象。

TreeMap也必须根据Comparable或者Comparator对自己的对象进行排序。

7.8 实用工具

min()、max()等。

7.8.1 使Collection或Map不可修改

通常,创建Collection或Map的一个“只读”版本显得更有利一些。Collections类允许我们达到这个目标,方法是将原始容器传递进入一个方法,并令其传回一个只读版本。这个方法共有四种变化形式,分别用于Collection(如果不想把集合当作一种更特殊的类型对待)、List、Set以及Map。

7.8.2 Collection或Map的同步

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容