Thinking in Java 3Edition 11:对象的集合 如果程序的对象数量有限,且寿命可知,那么这个程序是相当 简单的。 一般来说,程序都是根据具体情况在不断地创建新的对象,而这些情况又 只有在程序运行的时候才能确定。不到运行时你是不会知道你到底需要多 少对象,甚至是什么类型的对象。为了解决这种常见的编程问题,你得有 办法能在任何时间,任何地点,创建任何数量的对象。所以你不能指望用 命名的 reference来持有每个对象 Myobject 原因就在于,你不可能知道究竟需要多少这样的对象 针对这个相当关键的问题,绝大多数语言都提供了某种解决办法。Java 也提供了好几种持有对象(或者更准确的说,是对象的 reference)的方 法。我们前面讨论的数组是语言内置的数据类型。此外,Java的工具类 库还包括一套比较完整的容器类( container classes也被称为 co∥ lection classes,但是由于 Collection被Java2用来命名类库的 某个子集,所以我还是用概括性更强的术语" container")。它提供了复杂 而精致的方法来持有甚至是操控你的对象。 数组 我们已经在第4章的最后部分,对数组的绝大多数内容作了必要的介绍, 此外我们还演示了如何定义和初始化一个数组。本章的所关注的问题是 “持有对象”,而数组只是其中的一个方法。那么数组又是凭什么要我们 如此重视的呢? 数组与其它容器的区别体现在三个方面:效率,类型识别以及可以持有 primitives。数组是Java提供的,能随机存储和访问 reference序列的 诸多方法中的,最高效的一种。数组是一个简单的线性序列,所以它可以 快速的访问其中的元素。但是速度是有代价的;当你创建了一个数组之 后,它的容量就固定了,而且在其生命周期里不能改变。也许你会提议先 创建一个数组,等到快不够用的时候,再创建一个新的,然后将旧数组里 的 reference全部导到新的里面。其实(我们以后会讲的) Array List就 是这么做的。但是这种灵活性所带来的开销,使得 Array List的效率比 起数组有了明显下降 C++的 vector容器类确实能知道它到底持有了什么类型的对象,但是 与Java的数组相比,它又有另一个缺点:C++ vector的[]运算符不 第1页共106页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition 第 1 页 共 106 页 www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 11:对象的集合 如果程序的对象数量有限,且寿命可知,那么这个程序是相当 简单的。 一般来说,程序都是根据具体情况在不断地创建新的对象,而这些情况又 只有在程序运行的时候才能确定。不到运行时你是不会知道你到底需要多 少对象,甚至是什么类型的对象。为了解决这种常见的编程问题,你得有 办法能在任何时间,任何地点,创建任何数量的对象。所以你不能指望用 命名的 reference 来持有每个对象: MyObject myReference; 原因就在于,你不可能知道究竟需要多少这样的对象。 针对这个相当关键的问题,绝大多数语言都提供了某种解决办法。Java 也提供了好几种持有对象(或者更准确的说,是对象的 reference) 的方 法。我们前面讨论的数组是语言内置的数据类型。此外,Java 的工具类 库还包括一套比较完整的容器类(container classes 也被称为 collection classes,但是由于 Collection 被 Java 2 用来命名类库的 某个子集,所以我还是用概括性更强的术语"container")。它提供了复杂 而精致的方法来持有甚至是操控你的对象。 数组 我们已经在第 4 章的最后部分,对数组的绝大多数内容作了必要的介绍, 此外我们还演示了如何定义和初始化一个数组。本章的所关注的问题是 “持有对象”,而数组只是其中的一个方法。那么数组又是凭什么要我们 如此重视的呢? 数组与其它容器的区别体现在三个方面:效率,类型识别以及可以持有 primitives。数组是 Java 提供的,能随机存储和访问 reference 序列的 诸多方法中的,最高效的一种。数组是一个简单的线性序列,所以它可以 快速的访问其中的元素。但是速度是有代价的;当你创建了一个数组之 后,它的容量就固定了,而且在其生命周期里不能改变。也许你会提议先 创建一个数组,等到快不够用的时候,再创建一个新的,然后将旧数组里 的 reference 全部导到新的里面。其实(我们以后会讲的)ArrayList 就 是这么做的。但是这种灵活性所带来的开销,使得 ArrayList 的效率比 起数组有了明显下降。 C++的 vector 容器类确实能知道它到底持有了什么类型的对象,但是 与 Java 的数组相比,它又有另一个缺点:C++ vector 的[]运算符不
Chapter 11: Collections of Objects 作边界检查,所以你可能会不知不觉就过了界1。而Java对数组和容器都 做边界检查:如果过了界,它就会给一个 RuntimeException。这种 异常表明这个错误是由程序员造成的,这样你就用不着再在程序里面检查 了(译者注:这里的原文有些模棱两可,我想他要表达的意思可能是以下 两种中的一个。一是指,你不用再作边界检查了;二是指,通过异常信 息,你就可以知道错误是由过界造成的,因而不用查源代码了)。顺便说 下,C++的 vector不作边界检查是为了速度;而在Java,不论是数 组或是用容器,你都得面对边界检查所带来的固定的性能下降。 本章所探讨的其它泛型容器类还包括List,Set和Map。它们处理对象 的时候就好像这些对象都没有自己的具体类型一样。也就是说,容器将它 所含的元素都看成是(ava中所有类的根类) object的。这样你只需创 建一种容器,就能把所有类型的对象全都放进去。从这个角度来看,这种 做法很不错(只是苦了 primitive。如果是常量,你还可以用Java的 primitive的 wrapper类;如果是变量,那就只能放到你自己的类里 了)。与其他泛型容器相比,这里体现出数组的第二个优势:创建数组的 时候,你也同时指明了它所持有的对象的类型(这又引出了第三点——数 组可以持有 primitives,而容器却不行)。也就是说,它会在编译的时候 作类型检査,从而防止你插入错误类型的对象,或者是在提取对象的时候 把对象的类型给搞错了。Java在编译和运行时都能阻止你将一个不恰当 的消息传给对象。所以这并不是说使用容器就有什么危险,只是如果编译 器能够帮你指定,那么程序运行会更快,最终用户也会较少受到程序运行 异常的骚扰。 从效率和类型检查的角度来看,使用数组总是没错的。但是,如果你在解 决一个更为一般的问题,那数组就会显得功能太弱了点。本章先讲数组, 然后集中精力讨论Java的容器类。 数组是第一流的对象 不管你用的是那种类型的数组,数组的标识符实际上都是一个“创建在堆 (heap)里的实实在在的对象的” reference。实际上是那个对象持有其他 对象的 reference。你既可以用数组的初始化语句,隐含地创建这个对 象,也可以用new表达式,明确地创建这个对象。只读的 length属性 能告诉你数组能存储多少元素。它是数组对象的一部分(实际上也是你唯 能访问的属性或方法)。‘[]’语法是另一条访问数组对象的途径。 下面这段程序演示了几种初始化数组的办法,以及如何将数组的 reference赋给不同的数组对象。此外,它还显示了,对象数组和 primitives数组在使用方法上几乎是完全相同。唯一的不同是,对象数组 持有 reference,而 primitive数组则直接持有值。 7/: c11: Arraysize. java 不过真的想知道 vector的容量有多大,还是有办法的,更何况at)方法确实做边界检查 第2页共106页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Chapter 11: Collections of Objects 第 2 页 共 106 页 www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 作边界检查,所以你可能会不知不觉就过了界1。而 Java 对数组和容器都 做边界检查;如果过了界,它就会给一个 RuntimeException。这种 异常表明这个错误是由程序员造成的,这样你就用不着再在程序里面检查 了(译者注:这里的原文有些模棱两可,我想他要表达的意思可能是以下 两种中的一个。一是指,你不用再作边界检查了;二是指,通过异常信 息,你就可以知道错误是由过界造成的,因而不用查源代码了)。顺便说 一下,C++的 vector 不作边界检查是为了速度;而在 Java,不论是数 组或是用容器,你都得面对边界检查所带来的固定的性能下降。 本章所探讨的其它泛型容器类还包括 List,Set 和 Map。它们处理对象 的时候就好像这些对象都没有自己的具体类型一样。也就是说,容器将它 所含的元素都看成是(Java 中所有类的根类)Object 的。这样你只需创 建一种容器,就能把所有类型的对象全都放进去。从这个角度来看,这种 做法很不错(只是苦了 primitive。如果是常量,你还可以用 Java 的 primitive 的 wrapper 类;如果是变量,那就只能放到你自己的类里 了)。与其他泛型容器相比,这里体现出数组的第二个优势:创建数组的 时候,你也同时指明了它所持有的对象的类型(这又引出了第三点 —— 数 组可以持有 primitives,而容器却不行)。也就是说,它会在编译的时候 作类型检查,从而防止你插入错误类型的对象,或者是在提取对象的时候 把对象的类型给搞错了。Java 在编译和运行时都能阻止你将一个不恰当 的消息传给对象。所以这并不是说使用容器就有什么危险,只是如果编译 器能够帮你指定,那么程序运行会更快,最终用户也会较少受到程序运行 异常的骚扰。 从效率和类型检查的角度来看,使用数组总是没错的。但是,如果你在解 决一个更为一般的问题,那数组就会显得功能太弱了点。本章先讲数组, 然后集中精力讨论 Java 的容器类。 数组是第一流的对象 不管你用的是那种类型的数组,数组的标识符实际上都是一个“创建在堆 (heap)里的实实在在的对象的”reference。实际上是那个对象持有其他 对象的 reference。你既可以用数组的初始化语句,隐含地创建这个对 象,也可以用 new 表达式,明确地创建这个对象。只读的 length 属性 能告诉你数组能存储多少元素。它是数组对象的一部分(实际上也是你唯 一能访问的属性或方法)。‘[]’语法是另一条访问数组对象的途径。 下面这段程序演示了几种初始化数组的办法,以及如何将数组的 reference 赋给不同的数组对象。此外,它还显示了,对象数组和 primitives 数组在使用方法上几乎是完全相同。唯一的不同是,对象数组 持有 reference,而 primitive 数组则直接持有值。 //: c11:ArraySize.java 1不过真的想知道 vector 的容量有多大,还是有办法的,更何况 at( )方法确实做边界检查
Thinking in Java 3Edition Initialization re-assignment of arrays import com. bruceeckel. simpletest. class Weeble [) //A small mythical creature ublic class Arraysize private static Test monitor new Test( public static void main(String[] args)f // Arrays of objects Weeble[] a; / Local uninitialized variable Weeble[ b= new Weeble[5]; // Null references Weeble[ c= new Weeble[4]i for(int i =0; i< c. length; i++) if(c[il = null)// Can test for null referenc / Aggregate initialization Weeble[ d=i new Weeble(), new Weeble(), new Weeble( // Dynamic aggregate initialization a= new Weeble[ i new Weeble(), new Weeble( System. out. println("alength="+ alength)i System. out. println("blength ="+blength)i / The references inside the array are // automatically initialized to null for(int i =0; i< blength; i++) System. out. println(" c. length =+ clength)i System. out. println("d.lengt tln("a length t a length) // Arrays of primitives: int[l e; //Null t[]f t[5] t[ g w int [41 for(int i =0 g[i] 11,47,93} // Compile error: variable e not initialized //! System. out. println("e length="+ elength) Syst t println("f length =" f length)i / The primitives inside the array are automatically initialized to zero for (int 0; i<f length; i++) System. out. println("f["+ i +]=" f[i])i ngt g length) System. out.pr System. out System. out. println("elength ="+elength) monitor. expect(new String[] b lenath "b[1]=nu11", 第3页共106页 www.wgqqh.com/shhgs/tij.html
Thinking in Java 3rd Edition 第 3 页 共 106 页 www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com // Initialization & re-assignment of arrays. import com.bruceeckel.simpletest.*; class Weeble {} // A small mythical creature public class ArraySize { private static Test monitor = new Test( ); public static void main(String[] args) { // Arrays of objects: Weeble[] a; // Local uninitialized variable Weeble[] b = new Weeble[5]; // Null references Weeble[] c = new Weeble[4]; for(int i = 0; i < c.length; i++) if(c[i] == null) // Can test for null reference c[i] = new Weeble( ); // Aggregate initialization: Weeble[] d = { new Weeble( ), new Weeble( ), new Weeble( ) }; // Dynamic aggregate initialization: a = new Weeble[] { new Weeble( ), new Weeble( ) }; System.out.println("a.length=" + a.length); System.out.println("b.length = " + b.length); // The references inside the array are // automatically initialized to null: for(int i = 0; i < b.length; i++) System.out.println("b[" + i + "]=" + b[i]); System.out.println("c.length = " + c.length); System.out.println("d.length = " + d.length); a = d; System.out.println("a.length = " + a.length); // Arrays of primitives: int[] e; // Null reference int[] f = new int[5]; int[] g = new int[4]; for(int i = 0; i < g.length; i++) g[i] = i*i; int[] h = { 11, 47, 93 }; // Compile error: variable e not initialized: //!System.out.println("e.length=" + e.length); System.out.println("f.length = " + f.length); // The primitives inside the array are // automatically initialized to zero: for(int i = 0; i < f.length; i++) System.out.println("f[" + i + "]=" + f[i]); System.out.println("g.length = " + g.length); System.out.println("h.length = " + h.length); e = h; System.out.println("e.length = " + e.length); e = new int[] { 1, 2 }; System.out.println("e.length = " + e.length); monitor.expect(new String[] { "a.length=2", "b.length = 5", "b[0]=null", "b[1]=null
Chapter 11: Collections of Objects "b[2]=nu11 b[4]=nu11 d. length 3 h=3 "f length =5 f[1]=0 f[2]= "f[3]=0 th h length =3 数组a是一个尚未初始化的局部变量,在你将其正确地初始化之前,编译 器禁止你对这个 reference作任何事情。数组b是一个已经进行了初始 化的数组,它被连到了一个“ Weeble对象的 reference”的数组,只 是这个数组里面还没有真正放上 Weeble对象。但是由于b指向的是 个合法的对象,所以你已经可以査询其容量大小了。这就带来一个小问 题:你没法知道数组里面究竟放了多少元素,因为 length只是告诉你数 组能放多少元素,也就是说是数组对象的容量,而不是它真正已经持有的 元素的数量。但是,创建数组对象的时候,它所持有的 reference都会 被自动地初始化为nu,所以你可以通过检查数组的某个“槽位”是否 为nu,来判断它是否持有对象。以此类推, primitive的数组,会自动 将数字初始化为零,字符初始化为(char)0, boolean初始化为 数组c演示了数组对象的创建,随后它直接用 Weeble对象对数组各个 “槽位”进行赋值。数组d就是所谓“总体初始化( aggregate initialization)”的语法,它只用一条语句,就创建了数组对象(隐含地使 用了new,就像对数组c),并且用 Weeble对象进行了初始化 下一个数组的初始化可以被理解为“动态的总体初始化( dynamic aggregate initialization)”。d所使用的“总体初始化”语句,只能在 定义d的时候用。但是用这种语法,你就可以在任何地方创建和初始化数 组对象。比方说,假设hde()是一个使用 Weeble对象的数组做参数 的方法。那么,你可以这样调用: 第4页共106页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Chapter 11: Collections of Objects 第 4 页 共 106 页 www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com "b[2]=null", "b[3]=null", "b[4]=null", "c.length = 4", "d.length = 3", "a.length = 3", "f.length = 5", "f[0]=0", "f[1]=0", "f[2]=0", "f[3]=0", "f[4]=0", "g.length = 4", "h.length = 3", "e.length = 3", "e.length = 2" }); } } ///:~ 数组 a 是一个尚未初始化的局部变量,在你将其正确地初始化之前,编译 器禁止你对这个 reference 作任何事情。数组 b 是一个已经进行了初始 化的数组,它被连到了一个“Weeble 对象的 reference”的数组,只 是这个数组里面还没有真正放上 Weeble 对象。但是由于 b 指向的是一 个合法的对象,所以你已经可以查询其容量大小了。这就带来一个小问 题:你没法知道数组里面究竟放了多少元素,因为 length 只是告诉你数 组能放多少元素,也就是说是数组对象的容量,而不是它真正已经持有的 元素的数量。但是,创建数组对象的时候,它所持有的 reference 都会 被自动地初始化为 null,所以你可以通过检查数组的某个“槽位”是否 为 null,来判断它是否持有对象。以此类推,primitive 的数组,会自动 将数字初始化为零,字符初始化为(char)0,boolean 初始化为 false。 数组 c 演示了数组对象的创建,随后它直接用 Weeble 对象对数组各个 “槽位”进行赋值。数组 d 就是所谓“总体初始化(aggregate initialization)”的语法,它只用一条语句,就创建了数组对象(隐含地使 用了 new,就像对数组 c),并且用 Weeble 对象进行了初始化。 下一个数组的初始化可以被理解为“动态的总体初始化(dynamic aggregate initialization)”。d 所使用的“总体初始化”语句,只能在 定义 d 的时候用。但是用这种语法,你就可以在任何地方创建和初始化数 组对象。比方说,假设 hide( )是一个使用 Weeble 对象的数组做参数 的方法。那么,你可以这样调用: hide(d);
Thinking in Java 3Edition 但是你也可以动态地创建一个数组,把它当作参数传给hide() hide(new Weeble[]i new Weeble(), new Weeble()))i 在很多情况下,这能给你的编程带来便利。 表达式 演示了如何将一个 reference指向另一个数组对象。这么做跟使用其他 对象的 refernce没什么两样。现在a和d都指向堆中的同一个数组对 象 Arraysize.java的第二部分应征了 primitive数组的工作方式和对象 数组的几乎一摸一样。只是它能直接持有 primitive的值 primitive的容器 容器类只能持有 Object对象的 reference。而数组除了能持有 Objects的 reference之外,还可以直接持有 primitive。当然可以使 用诸如 Integer, Double之类的 wrapper类,把 primitive的值放到 容器中,但这样总有点怪怪的。此外, primitive数组的效率要比 wrapper类容器的高出许多。 当然,如果你使用 primitive的时候,还需要那种“能随需要自动扩展 的”容器类的灵活性,那就不能用数组了。你只能用容器来存储 primitive的 wrapper类。也许你会想,应该为每种 primitive都提供 个 Array List,但是遗憾的是Java没为你准备。2 返回一个数组 假设你写了一个方法,它返回的不是一个而是一组东西。在C和C++之 类的语言里,这件事就有些难办了。因为你不能返回一个数组,你只能返 回一个指向数组的指针。由于要处理“控制数组生命周期”之类的麻烦 事,这么做很容易会出错,最后导致内存泄漏。 Java采取了类似的解决方案,但是不同之处在于,它返回的“就是一个 数组”。与C++不同,你永远也不必为Java的数组操心—一只要你还 需要它,它就还在;一旦你用完了,垃圾回收器会帮你把它打扫干净。 2这就是C+明显比Ja强的地方了,因为它的 template关键词支持参数化类型( parameterized type) 第5页共106页 www.wgqqh.com/shhgs/tij.html emailshhgsasohu.com
Thinking in Java 3rd Edition 第 5 页 共 106 页 www.wgqqh.com/shhgs/tij.html email:shhgs@sohu.com 但是你也可以动态地创建一个数组,把它当作参数传给 hide( ): hide(new Weeble[] { new Weeble( ), new Weeble( ) }); 在很多情况下,这能给你的编程带来便利。 表达式: a = d; 演示了如何将一个 reference 指向另一个数组对象。这么做跟使用其他 对象的 refernce 没什么两样。现在 a 和 d 都指向堆中的同一个数组对 象。 ArraySize.java 的第二部分应征了 primitive 数组的工作方式和对象 数组的几乎一摸一样。只是它能直接持有 primitive 的值。 primitive 的容器 容器类只能持有 Object 对象的 reference。而数组除了能持有 Objects 的 reference 之外,还可以直接持有 primitive。当然可以使 用诸如 Integer,Double 之类的 wrapper 类,把 primitive 的值放到 容器中,但这样总有点怪怪的。此外,primitive 数组的效率要比 wrapper 类容器的高出许多。 当然,如果你使用 primitive 的时候,还需要那种“能随需要自动扩展 的”容器类的灵活性,那就不能用数组了。你只能用容器来存储 primitive 的 wrapper 类。也许你会想,应该为每种 primitive 都提供一 个 ArrayList,但是遗憾的是 Java 没为你准备。2 返回一个数组 假设你写了一个方法,它返回的不是一个而是一组东西。在 C 和 C++之 类的语言里,这件事就有些难办了。因为你不能返回一个数组,你只能返 回一个指向数组的指针。由于要处理“控制数组生命周期”之类的麻烦 事,这么做很容易会出错,最后导致内存泄漏。 Java 采取了类似的解决方案,但是不同之处在于,它返回的“就是一个 数组”。与 C++不同,你永远也不必为 Java 的数组操心——只要你还 需要它,它就还在;一旦你用完了,垃圾回收器会帮你把它打扫干净。 2这就是 C++明显比 Java 强的地方了,因为它的 template 关键词支持参数化类型(parameterized type)