数组

数组与其它种类的容器之间的区别有三方面:效率、类型和持有基本类型的能力。在Java中,数组是一种效率最高的存储和随机访问对象引用序列的方式。数组就是个简单的线性序列,这使得元素访问非常快速,但也损失了其他一些特性。当你创建了一个数组对象(将数组本身作为对象看待),数组的大小就被固定了,并且这个数组的生命周期也是不可改变的。通常是创建一个特定大小的数组,在空间不足的时候再创建一个新的数组,然后把
旧数组中所有的引用移到新数组中。这正是后面会学到的ArrayList 类的行为方式。然而这种弹性带来的开销,使得ArrayList 比数组效率低。

考虑到效率与类型检查,应该尽可能使用数组。然而,如果要解决更一般化的问题,数组就太受限制了。

数组是第一级对象

无论使用哪种数组,数组标识符其实只是一个引用,指向在堆(heap)中创建的一个真实对象,这个(数组)对象用以保存指向其他对象的引用。

对象数组和基本类型数组在使用上几乎是同样的。唯一的区别就是对象数组保存的是引用,基本类型数组直接保存基本类型的值。

你无法知道在此数组中确切地有多少元素,因为length 只表示数组能够容纳多少元素。也就是说,length 是数组的大小,而不是实际保存的元素个数。新生成一个数组对象时,其中所有的引用被自动初始化为null,所以检查其中的引用是否为null,即可知道数组的某个位置是否存有对象。同样地,基本类型的数组如果是数值型的,就被自动初始化为0,字符型(char)数组被初始化为(char)0,布尔(boolean)数组被初始化为false。

a = d;演示了如何将指向某个数组对象的引用赋值指向另一个数组对象,这与其他类型对象的引用没什么区别。现在a 与d 都指向堆中的同一个数组对象。

ArraySize.java 的第二部分说明,基本类型的数组的工作方式与对象数组一样,不过基本类型的数组直接存储基本类型数据的值。

基本类型的容器

容器类只能保存对象的引用。而数组可以像保存对象的引用一样,直接保存基本类型。在容器中可以使用“包装”类,例如Integer,Double 等,以代替基本类型的值。但是相对于基本类型,包装类使用起来很笨拙。此外,与包装过的基本类型的容器相比,创建与访问一个基本类型的数组效率更高。

方法flavorSet()创建了一个名为results 的String 数组。此数组容量为n,由传入方法的参数决定。然后从数组flavors 中随机地选择元素,存入results 中,此方法最终返回results 数组。返回一个数组与返回任何其他对象(本质是引用)没什么区别。数组是在flavorSet()中被创建的,还是在别的地方被创建的并不重要。当你使用完毕后,垃圾回收器负责清除数组,而只要你还需要它,此数组就会一直存在。

Arrays 类

在 java.util 类库中可以找到Arrays 类,它有一套static 方法,提供操作数组的实用功能。其中有四个基本方法:equals(),比较两个数组是否相等;fill(),用某个值填充整个数组;sort(),对数组排序;还有binarySearch(),在已经排序的数组中查找元素。所有这些方法都被各种基本类型和Object 类重载过。此外,方法asList()接受任意的数组为参数,并将其转变为List 容器(稍后会学到List)。

toString()的代码展示了如何使用StringBuffer 代替String 对象,这是基于效率考虑;如果你多次调用一个方法,其中需要组装字符串,那么使用更高效的StringBuffer 取代String 就是明智之举。此处的StringBuffer,在创建的时候就带有初始值,然后再向其中添加String。

public static String toString(float[] a) {
StringBuffer result = new StringBuffer("[");
for(int i = 0; i < a.length; i++) {
result.append(a[i]);
if(i < a.length - 1)
result.append(", ");
}
result.append("]");
return result.toString();
}

复制数组

Java 标准类库提供有static 方法System.arraycopy(),用它复制数组比用for 循环复制要快很多

arraycopy()需要的参数有:源数组、表示从源数组中的什么位置开始复制的偏移量、表示从目标数组的什么位置开始复制的偏移量、以及需要复制的元素个数。当然,对数组的任何越界操作都会导致异常。

这个例子说明基本类型数组与对象数组都可以复制。然而,如果你复制对象数组,那么只是复制了引用——不会出现两份对象的拷贝。这被称作浅复制(shallow copy)

数组的比较

Arrays 类重载了equals()方法,用来比较整个数组。同样的,此方法被所有基本类型与Object 都作了重载。数组相等的条件时元素个数必须相等,并且对应位置的元素也相等,这可以通过对每一个元素使用equals()做比较来判断。(对于基本类型,需要使用基本类型的包装类的equals()方法,例如,对于int 类型使用Integer.equals()作比较

数组元素的比较

Java 有两种方式提供比较功能。第一种是实现java.lang.Comparable 接口,使你的类具有“天生”的比较能力。此接口很简单,只有compareTo()一个方法。此方法以要比较的Object 为参数,如果当前对象小于参数则返回负值,如果相等则返回零,如果当前对象大于参数则返回正值。

 容器简介

Java 2 容器类类库的用途是“持有你的对象”,并将其划分为两个不同的概念:

1. Collection: 一组独立的元素,通常有某种规则应用于其上。List 必须保持元素特定的顺序,而Set 不能有重复元素。

2.Map: 一组成对的键值对(key-value)对象。Map 可以返回所有键组成的Set,所有值组成的Collection,或其键值对组成的Set;

容器只保存Object 型的引用,这是所有类的基类,因此容器可以保存任何类型的对象。(当然不包括基本类型,因为它们不是真正的对象,没有继承任何东西。)

好在 Java 并不会让你误用容器中的对象。如果你将“狗”丢入存放“猫”的容器,然后将其中的每件东西都作为“猫”,当你将指向“狗”的引用取出容器,类型转换为“猫”时会收到RuntimeException 异常

迭代器

迭代器的概念(也是一种设计模式)可以用来达成此目的。迭代器是一个对象,它的工作是遍历并选择序列中的对象

Java 的Iterator 就是迭代器受限制的例子,它只能用来:

1. 使用方法 iterator()要求容器返回一个Iterator。第一次调用Iterator 的next()方法时,它返回序列的第一个元素。

2.使用 next()获得序列中的下一个元素。
3.使用 hasNext()检查序列中是否还有元素。
4.使用 remove()将上一次返回的元素从迭代器中移除。

Iterator e = cats.iterator();
while(e.hasNext())
((Cat)e.next()).id();

可以看到,程序最后几行不再使用for 循环,而是使用Iterator 遍历整个序列。有了Iterator就不必为容器中元素的数量操心,由hasNext()和next()为你照看着。

public class InfiniteRecursion {
public String toString() {
return " InfiniteRecursion address: " + this + "\n";
}

String 的自动类型转换,如果你这样写:"InfiniteRecursion address: " + this编译器见到String 后跟着一个’+’号,而’+’后的对象却不是String,于是编译器尝试将this转变成String 类型。此类型转换操作调用的是toString()方法,于是产生递归调用。

在此例中,如果你确实想打印对象的地址,应该调用 Object 的toString()方法,它专门做此工作。因此使用super.toString()取代this 即可。

任意的Collection 可以生成Iterator,而List 可以生成ListIterator(也能生成普通的Iterator,因为List 继承自Collection)。

与持有对象有关的接口是 Collection,List,Set 和Map。最理想的情况是,你的代码只与这些接口打交道,仅在创建容器的时候说明容器的特定类型。因此可以这样创建一个List: List x = new LinkedList();

Collection 的功能方法

public static void iterMotion(List a) {
ListIterator it = a.listIterator();
b = it.hasNext();
b = it.hasPrevious();
o = it.next();
i = it.nextIndex();
o = it.previous();
i = it.previousIndex();
}

使用 LinkedList 制作一个栈

public class StackL {
private static Test monitor = new Test();
private LinkedList list = new LinkedList();
public void push(Object v) { list.addFirst(v); }
public Object top() { return list.getFirst(); }
public Object pop() { return list.removeFirst(); }
public static void main(String[] args) {
StackL stack = new StackL();
for(int i = 0; i < 10; i++)
stack.push(Collections2.countries.next());
System.out.println(stack.top());
System.out.println(stack.top());
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());

使用 LinkedList 制作一个队列

public class Queue {
private static Test monitor = new Test();
private LinkedList list = new LinkedList();
public void put(Object v) { list.addFirst(v); }
public Object get() { return list.removeLast(); }
public boolean isEmpty() { return list.isEmpty(); }
public static void main(String[] args) {
Queue queue = new Queue();
for(int i = 0; i < 10; i++)
queue.put(Integer.toString(i));

while(!queue.isEmpty())
System.out.println(queue.get());

Set 的功能方法

Set (interface)

存入Set 的每个元素都必须是唯一的,因为Set 不保存重复元素。加入Set 的元素必须定义equals()方法以确保对象的唯一性。Set 与Collection 有完全一样的接口。Set 接口不保证维护元素的次序。

HashSet

为快速查找设计的Set。存入HashSet 的对象必须定义hashCode()。

TreeSet

保持次序的 Set,底层为树结构。使用它可以从Set中提取有序的序列

LinkedHashSet

具有HashSet 的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。

"HashSet",
"[one, two, five, four, three, seven, six]",
"[one, two, five, four, three, seven, six]",
"s.contains(\"one\"): true",
"TreeSet",
"[five, four, one, seven, six, three, two]",
"[five, four, one, seven, six, three, two]",
"s.contains(\"one\"): true",
"LinkedHashSet",
"[one, two, three, four, five, six, seven]",
"[one, two, three, four, five, six, seven]",
"s.contains(\"one\"): true"

运行此程序,你会注意到,HashSet 维护的元素次序不同于TreeSet 和LinkedHashSet,因为它们保存元素的方式各有不同,使得以后还能找到元素。(TreeSet 采用红黑树的数据结构排序元素,HashSet 则采用散列函数,这是专门为快速查询设计的。LinkedHashSet内部使用散列以加快查询速度,同时使用链表维护元素的次序,使得看起来元素是以插入的顺序保存的。)

本章稍后会介绍如何定义equals()和hashCode()。使用以上两种Set 都必须为你的类定义equals(),而hashCode(),只在你的类会被HashSet 用到的情况下才是必要的(这种可能性很大,因为HashSet 通常是使用Set 的第一选择。无论如何,作为一种编程风格,当你重载equals()的时候,就应该同时重载hashCode()。

SortedSet

使用SortedSet(TreeSet 是其唯一的实现),可以确保元素处于排序状态,还可以使用SortedSet 接口提供的附加功能:

Comparator comparator(): 返回当前 Set 使用的Comparator,或者返回null,表示以自然方式排序。

Map 的功能方法

由“从序列中进行选择”这种思想发展出了map 这种强大的工具,也称作字典,执行效率是Map的一个大问题。看看get()要做哪些事,就会明白为什么在ArrayList中搜索“键”是相当慢的。而这正是HashMap提高速度的地方。HashMap使用了特殊的值,称作“散列码”(hash code),来取代对“键”的缓慢搜索。“散列码”是“相对唯一”用以代表对象的int值,它是通过将该对象的某些信息进行转换而生成的。所有Java对象都能产生散列码,因为hashCode()是定义在基类Object中的方法。HashMap就是使用对象的hashCode()进行快速查询的。此方法能够显著提高性能。

class Counter {
int i = 1;
public String toString() { return Integer.toString(i); }
}
public class Statistics {
private static Random rand = new Random();
public static void main(String[] args) {
Map hm = new HashMap();
for(int i = 0; i < 10000; i++) {
// Produce a number between 0 and 20:
Integer r = new Integer(rand.nextInt(20));
if(hm.containsKey(r))
((Counter)hm.get(r)).i++;
else
hm.put(r, new Counter());
}
System.out.println(hm);
}
}

在main()中,每生成一个随机数就用Integer 对象包装起来,这样HashMap 才能使用。(容器不能保存基本类型,只能保存对象的引用。)containsKey()方法会检查此“键”是否已经在容器中了(也就是说,此随机数是否已经生成过了)。如果找到了,get()方法返回当前与“键”相关联的“值”,就是Counter 对象。Counter 中的i 值增加时表示当前的随机数又出现了一次。

要显示 HashMap,只需直接打印。HashMap 的toString()方法会遍历所有的键值对,并对每一个键值对调用toString()        {15=529, 4=488, 19=518, 8=487, 11=501, 16=487, 18=507, 3=524,
7=474, 12=485, 17=493, 2=490, 13=540, 9=453, 6=512, 1=466,
14=522, 10=471, 5=522, 0=531}

LinkedHashMap

为了提高速度,LinkedHashMap 散列化所有的元素,但是在遍历“键值对”时,却又以元素的插入顺序返回“键值对”(println()会迭代遍历Map,因此你可以看到遍历的结果)

散列算法与散列码

这看起来够简单了,但是它不工作。问题出在Groundhog 继承自基类Object(如果你不特别指定父类,任何类都会自动继承自Object,因此所有的类最终都继承自Object)。所以这里是使用Object 的hashCode()方法生成散列码,而它默认是使用对象的地址计算散列码。因此,由Groundhog(3)生成的第一个实例的散列码与由Groundhog(3)生成的第二个实例的散列码是不同的,而我们正是使用后者进行查找的。

HashMap 使用equals()判断当前的“键”是否与表中存在的“键”相同。

再说一次,默认的 Object.equals()只是比较对象的地址,所以一个Groundhog(3)并不等于另一个Groundhog(3)。因此,如果要使用自己的类作为HashMap 的“键”,你必须同时重载hashCode()和equals()。

public class Groundhog2 extends Groundhog {
public Groundhog2(int n) { super(n); }
public int hashCode() { return number; }
public boolean equals(Object o) {
return (o instanceof Groundhog2)
&& (number == ((Groundhog2)o).number);
}
} ///:

理解 hashCode()

散列的价值在于速度:散列使得查询得以快速进行。由于速度的瓶颈是对“键”的查询,因此解决方案之一就是保持“键”的排序状态。

数组并不保存“键”本身。而是通过“键”对象生成一个数字,将其作为数组的下标索引。这个数字就是散列码,由定义在Object 中的hashCode()生成(在计算机科学的术语中称为散列函数)。你的类总是应该重载hashCode()方法。为解决数组容量被固定的问题,不同的“键”可以产生相同的下标。也就是说,可能会有冲突(collision)。因此,数组多大就不重要了,每个“键”总能在数组中找到它的位置

HashMap 的性能因子

容量(Capacity):散列表中桶的数量。

选择接口的不同实现

Hashtable、Vector 和Stack 的“特征”是,它们是过去遗留下来的类,目的只是为了支持老的程序罢了。因此,最好不要在新的程序中使用它们。

容器之间的区别,通常归结为由什么在背后“支持”它们。也就是说,你使用的接口是由什么样的数据结构实现的。例如,ArrayList 和LinkedList 都实现了List 接口,因此无论选择哪一个,基本操作都是相同的。然而,ArrayList 底层由数组支持;而LinkedList 是由双向链表实现的,其中的每个对象包含数据的同时,还包含指向链表中前一个与后一个元素的引用。因此,如果要经常在list 中插入或删除元素,LinkedList 就比较合适。(LinkedList 还有建立在AbstractSequentialList 基础上的其他功能。)否则,应该使用速度更快的ArrayList。

再举个例子,Set 可被实现为TreeSet,HashSet,或LinkedHashSet。每一种都有不同的行为:HashSet 是最常用的,查询速度最快;LinkedHashSet 保持元素插入的次序;TreeSet 基于TreeMap,生成一个总是处于排序状态的Set。由此,你可以根据所需的行为来选择接口不同的实现。通常,使用HashSet 就足够了,它是使用Set 的默认首选.

对 List 的选择

对于随机查询与迭代遍历操作,数组比所有的容器都要快。可以看到,对于随机访问(get()),ArrayList 的开销小于LinkedList。(奇怪的是,对于迭代遍历操作,LinkedList 比ArrayList 要快,这有点有悖于直觉。)另一方面,从中间的位置插入和删除元素,LinkedList 要比ArrayList 快,特别是删除操作。Vector 通常不如ArrayList
快,而且应该避免使用.

最佳的做法是将ArrayList 作为默认首选,只有当程序的性能因为经常从list 中间进行插入和删除而变差的
时候,才去选择LinkedList。当然了,如果只是使用固定数量的元素,就应该选择数组。

对 Set 的选择

HashSet 的性能总是比TreeSet 好(特别是最常用的添加和查询元素操作)。TreeSet存在的唯一原因是,它可以维持元素的排序状态。所以,只有当你需要一个排好序的Set时,才应该使用TreeSet。

注意,对于插入操作,LinkedHashSet 比HashSet 略微慢一点;这是由维护链表所带来额外开销造成的。不过,因为有了链表,遍历LinkedHashSet 会更快。

List 的排序和查询

List 排序与查询所使用的方法与对象数组所使用的方法有相同的名字与语法,只是用Collections 的static 方法代替Arrays的方法而已.Collections.shuffle(list);

AlphabeticComparator comp = new AlphabeticComparator();
Collections.sort(list, comp);

重载 hashCode()

首先,你无法控制 bucket 数组的索引值的产生。这个值依赖于具体的HashMap 对象的容量,而容量的改变与负载因子和容器有多满有关。hashCode()生成的结果,经过处理后成为“桶”的索引(在SimpleHashMap 中,只是对其取模,模数为bucket 数组的大小)。

设计 hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该生成同样的值。如果在将一个对象用put()添加进HashMap 时,产生一个hashCode()值,而用get()取出时,却产生了另一个hashCode()值,那么你就无法重新取得该对象了。所以,如果你的hashCode()依赖于对象中易变的数据,用户就必须当心了,因为此数据发生变化时,hashCode()就会生成一个不同的散列码,相当于产生了一个不同的“键”.

由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方 法。hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相 同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如 果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希 表的操作。

如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。