Java学习笔记——持有对象

本文为Java编程思想第四版的学习笔记,在此感谢作者Bruce Eckel给我们带来这样一本经典著作,也感谢该书的翻译及出版人员。本文为原创笔记,转载请注明出处,谢谢。


如果一个程序只包含固定数量的且其生命周期都是已知的对象,那么这时一个非常简单的程序。通常,程序总是根据运行时才知道的某些条件去创建新对象。在此之前,不会知道所需对象的数量,甚至不知道确切的类型。为解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象,所以,就不能依靠创建命名的引用来持有每一个对象,因为你不知道实际上会需要多少这样的引用。大多数语言都提供某种方法来解决这个基本问题。Java有多种方式保存对象(应该说是对象的引用)。例如前面曾经学习过的数组,它是编译器支持的类型。数组是保存一组对象的最有效的方式,如果你想保存一组基本类型数据,也推荐使用这种方式。但是数组具有固定的尺寸,而在一般的情况中,你在写程序时并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,因此数组尺寸固定这一限制显得过于受限了。

Java使用类库还提供了一套相当完整的容器类来解决这个问题,其中基本的类型是List、Set、Queue和Map。这些对象类型也成为集合类,但由于Java的类库中使用了Collection这个名字来指代该类库的一个特殊子集,所以我使用了范围更广的术语“容器”来称呼他们。容器提供了完善的方法来保存对象,你可以使用这些工具来解决数量惊人的问题。

容器还有其他一些特性。例如,Set对于每个值都只保存一个对象,Map是允许你将某些对象与其他一些对象关联起来的关联数组,Java容器类都可以自动地调整自己的尺寸。因此,与数组不同,在编程时,你可以将任意数量的对象放置到容器中,并且不需要担心容器应该设置为多大。


1.泛型和类型安全容器

使用Java SE5之前的容器的一个主要问题就是编译器允许你想容器中插入不正确的类型。Java SE5中加入了泛型,利用泛型,你可以指定容器实例可以保存的类型。通过使用泛型,局可以在编译期防止将错误类型的对象放置到容器中。


2.基本概念

Java容器类雷孔的用途是“保存对象”,并将其划分为两个不同的概念:

1)Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照排队规则来确定对象产生的顺序(通常与他们被插入的顺序相同)。

2)Map。一组成对的“键值对”对象,允许你使用键来查找值。ArrayList允许你使用数字来查找值,因此在某种意义上讲,它将数字与对象关联在了一起。映射表允许我们私用另一个对象来查找某个对象,它也被称为“关联数组”,因为它将某些对象与另外一些对象关联在了一起;或者被称为“字典”,因为你可以使用键对象来查找值对象,就像在字典中使用单词来定义一样。Map是强大的编程工具。

尽管并非总是这样,但是在理想情况下,你编写的大部分代码都是在与这些接口打交道,并且你唯一需要制定所使用的精确类型的地方就是在创建的时候。因此,你可以像下面这样创建一个List:

List<Apple> apples = new ArrayList<apple>();

注意,ArrayList已被向上转型为List。使用接口的目的在与如果你决定去修你的视线,你所需的只是在创建处修改它,就像现在这样:

List<apple> apples = new LinkedList<apple>();

因此,你应该创建一个具体类的对象,将其转型为队形的接口,然后在其余的代码中都使用这个接口。

这种方式并非总能奏效,因为某些类具有额外的功能,例如,LinkedList具有在List几口中未包含的额外方法,而TreeMap也具有在Map中未包含的方法。如果你需要使用这些方法,就不能将它们向上转型为更通用的接口。

Collection接口概括了序列的概念——一种存放一组对象的方式。


3.添加一组元素

在java.util包中的Arrays和Collections类中都有很多使用的方法,可以再一个Collection中添加一组元素。Arrays.asList()方法接收一个数组伙食一个用逗号分隔的元素列表(使用可变参数),并将其转换为一个List对象。Collection.addAll()方法接受一个Collection对象,以及一个数组或是一个用逗号分割的列表,将元素添加到Collection中。


4.容器的打印

你必须使用Arrays.toString()来产生数组的可打印表示,但是打印容器无需任何帮助。默认的打印行为(使用容器提供的toString()方法)即可生成可读性很好的结果。Collection打印出来的内容使用方括号括住,每个元素由逗号分隔。Map则用大括号括住,键与值有等号联系(键在等号左边,值在右边)。

ArrayList和LinkedList都是List类型,它们都按照被插入的顺序保存元素。两者的不同之处不仅在于执行某些类型的操作时的性能,而且LinkedList包含的操作也多余ArrayList。

HashSet、TreeSet、和LinkedHashSet都是Set类型,每个相同的项只有保存一次,但是输出也显示了不同的Set实现存储元素的方式也不同。HashSet使用的是相当复杂的方式来存取元素的,此刻你只要知道这种技术是最快的获取元素方式,因此,存储的顺序看起来并无实际意义(通常你只会关心某事物是否是某个Set的成员,而不会关心它在Set出现的顺序)。如果存储顺序很重要,那么可以使用TreeSet,它按照比较结果的升序保存对象;或者使用LinkedHashSet,它按照被添加的顺序保存对象。

Map(也被称为关联数组)使得你可以用键值来查找对象,就像一个简单的数据库。键所关联的对象成为值,正是由于这种行为,对于每一个键,Map只接受存储一次。注意,你不必指定或考虑Map的尺寸,因为它自己会自动调整尺寸。Map还知道如何打印自己,它会显示相关联的键和值。键和值在Map中的保存书序并不是它们的插入顺序,因为HashMap实现使用的是一种非常快的算法来控制顺序。HashMap、TreeMap和LinkedHashMap是三种不同风格的Map。与HashSet一样,HashMap也提供了最快的查找技术,也没有按照任何明显的顺序来保存其元素。TreeMap按照比较结果升序保存键,而LinkedHashMap则按照插入的顺序保存键,同时还保留了HashMap的查询速度。


5.List

List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上添加了大量的方法,使得可以在List的中间插入和移动元素。有两种类型的List:

1)基本的ArrayList,它擅长于随机访问元素,但是在List的中间插入和移动元素时较慢。

2)LinkedList,它通过代价较低的在List中间进行的插入和删除操作,提供够了优化顺序访问。LinkedList在随机访问方面相对比较慢,但是特性集较ArrayList更大。


6.迭代器

任何容器类,都必须有某种方式可以插入元素并将它们再次取回。毕竟,持有事物是容器最基本的工作。对于List,add()是插入元素的方法之一,而get()是取出元素的方法之一。

如果从更高层的角度思考,会发现这里有缺点:要使用容器,必须对容器的确切类型编程。初看起来,这没什么不好,但是考虑下面的情况:如果原本是对象List编码的,但是后来发现如果能够把相同的代码应用于Set,将会显得非常方便,此时应该怎么做?或者打算从头开始编写通用的代码,它们只是使用容器,不知道或者说不关心容器的类型,那么如何才能不重写代码就可以应用于不同类型的容器呢?

迭代器(也是一种设计模式)的概念可以用于达成此目的。迭代器是一个对象,它的工作是遍历并选择序列中的对象,而客户端程序员不必知道或关心该序列底层的结构。此外,迭代器通常被称为轻量级对象:创建它的代价小。因此,经常可以见到对迭代器有些奇怪的限制:例如,Java的Iterator只能单向移动,这个Iterator只能用来

1)使用方法iterator()要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素。

2)使用next()获得序列中的下一个元素。

3)使用hasNext()检查序列中是否还有元素

4)使用remove()将迭代器新近返回的元素删除

如果你只是向前遍历List,并不打算修改List对象本身,那么你可以看到foreach语法将会显得更加简洁。


ListIterator是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。尽管Iterator只能向前移动,但是ListIterator可以双向移动。它还可以产生相对于迭代器在列表中指向当前位置的前一个和后一个元素的索引,并且可以使用set()方法替换它访问过的最后一个元素。你可以通过调用listIterator()方法产生一个指向List开始处的ListIterator,并且还可以通过调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。


7.LinkedList

LinkedList也像ArrayList一样实现了基本的List接口,但是它执行某些操作(在List的中间插入和移除)时比ArrayList更高效,但在随机访问操作方面却要逊色一些。

LinkedList还添加了可以使其用作栈、队列或者双端队列的方法。


8.Stack

“栈”通常是指“后进先出”(LIFO)的容器。有时栈也被称为叠加栈,因为最后“压入”栈的元素,第一个“弹出”栈。经常用来类比栈的事物是装有弹簧的存储器中的自助餐托盘,最后装入的托盘总是被最先拿出来使用。


9.Set

Set不保存重复的元素(至于如何判断元素相同则较为复杂,稍后便会看到)。如果你试图将相同对象的多个实例添加到Set中,那么它就会组织这种重复现象。Set中最常被使用的是测试归属性,你可以很容易地询问某个对象是否在某个Set中。正因如此,查找就成为了Set中最重要的操作,因此你通常都会选择一个HashSet的实现,它专门对快速查找进行了优化。

Set具有与Collection完全一样的接口,因为没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只是行为不同(这是继承与多态思想的典型应用:表现不同的行为)。Set是基于对象的值来确定归属性的。


10.Map

将对象映射到其他对象的能力是一种解决编程问题的杀手锏。Map与数组和其他的Collection一样,可以很容易地扩展到多维,而我们只需将其值设置为Map(这些Map的值可以是其他容器,甚至可以是其他的Map)。因此,我们能够很容易地将容器组合起来从而快速地生成强大的数据结构。Map可以返回它的键的Set,它的值的Collection,或者它的键值对的Set。


11.Queue

队列是一个典型的先进先出(FIFO)的容器。即从容器的一段放入事物,从另一端取出,并且事物放入容器的顺序与取出的书序是相同的。队列常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发中特别重要。

先进先出描述了最典型的队列规则。队列规则是指在给定一组队列中的元素的情况下,确定下一个弹出队列的元素规则。先进先出声明的是下一个元素应该是等待时间最长的元素。优先级队列(PriorityQueue)声明下一个弹出元素时最需要的元素(具有最高的优先级)。当你在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序,但是你可以通过提供自己的Comparator来修改这个默认书序。PriorityQueue可以确保当你调用peek()、poll()、remove()方法时,获取的元素僵尸队列中优先级最高的元素。


12.Collection和Iterator

Collection是你啊搜狐所有序列容器的共性根接口,它可能会被认为是一个“附属接口”,即因为要表示其他若干个忌口的共性而出现的接口。另外,java.util.AbstractCollection类提供了Collection的默认表现,使得你可以创建AbstractCollection的子类型,而其中没有不必要的代码重复。

使用接口描述的一个理由是它可以使我们能够创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多的对象类型。因此,如果我编写的方法将接受一个Collection,那么该方法就可以应用于任何实现了Collection的类——这也就使得一个新类可以选择去实现Collection接口,以便我的方法可以使用它。但是,有一点很有趣,就是我们注意到标准C++类库中并没有容器的任何公共基类——容器之间的所有共性都是通过迭代器达成的。在Java中,遵循C++的方式看起来似乎很明智,即用迭代器而不是Collection来表示容器之间的共性。但是,这两种方法绑定到了一起,因为实现Collection就一位置需要提供iterator()方法。

当你要实现一个不是Collection的外部类时,由于让它去实现Collection可能非常困难或麻烦,因此使用Iterator就会变得非常吸引人。


13.Foreach与迭代器

到目前为止,foreach语法主要用于数组,但是它也可以用于任何Collection对象。之所以能够工作,是因为Java SE5引入了新的被称为Iterable的接口,该接口包含一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。因此如果你创建了任何实现Iterable的类,都可以将他用于foreach语句中。

在Java SE5中,大量的类都是Iterable类型,主要包括所有的Collection类(但是不包括各种Map)。foreach语句可以用于数组或其他任何Iterable,但是这并不意味着数组肯定也是一个Iterable,而任何自动包装也不会发生。尝试把数组当做一个Iterable参数传递会导致失败。这说明不存在任何从数组到Iterable的自动转换,你必须手工执行这种转换。


13.1 适配器方法惯用法

如果现有一个Iterable类,你想要添加一种或多种在foreach语句中使用这个类的方法,应该怎么做呢?例如,假设你希望可以选择以向前的方向或是向后的方向迭代一个单词列表。如果直接继承这个类,并且覆盖iterator()方法,你只能替换现有的方法,而不能实现选择。

一种解决方案是所谓适配器方法的惯用法。“适配器”部分来自于设计模式,因为你必须提供特定接口以满足foreach语句。当你有一个接口并需要另一个接口时,编写适配器就可以解决问题。


14.总结

Java提供了大量持有对象的方式:

1)数组将数字与对象联系起来。它保存类型明确的对象,查询对象时,不需要对结果做类型转换。它可以是多维的,可以保存基本数据类型的数据。但是,数组一旦生成,其容量就不能改变。

2)Collection保存单一的元素,而Map保存相关联的键值对。有了Java的泛型,你就可以指定容器中存放的对象类型,因此你就不会将错误类型的对象放置到容器中,并且在从容其中获取元素时,不必进行类型转换。各种Collection和各种Map都可以在你向其中添加跟多的元素时,自动调整其尺寸。容器不能持有基本类型,但是自动包装机制会仔细地执行基本类型到容器中所有的包租昂其类型之间的双向转换。

3)像数组一样,List也建立数字索引与对象的关联,因此,数组和List都是排好序的容器。List能自动扩充容量。

4)如果要进行大量的随机访问,就用ArrayList;如果经常从表中间插入或删除元素,则应该使用LinkedList。

5)各种Queue以及栈的行为,有LinkedList提供支持。

6)Map是一种将对象(而非数组)与对象相关联的设计。HashMap设计用来快速访问;而TreeMap保持“键”始终处于排序状态,所以没有HashMap快。LinkedListHashMap保持元素插入的顺序,但是也通过散列提供了快速访问的能力。

7)Set不接受重复元素。HashSet提供最快的查询速度,而TreeSet保持元素处于排序状态。LinkedHashSet以插入顺序保持元素。

8)新程序中不应该使用过时的Vector、Hashtable和Stact。












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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值