欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

88.容器类

程序员文章站 2022-04-28 21:20:10
...

一个容器(container)是用一个对象来“存放”一组对象,其中的每个对象作为容器的一个元素。所有的容器类都放在java.util包中。


在Java 2 中,对JDK1.0和JDK1.1中的容器工具作了重新的设计,它由《Effective Java》的作者Joshua Bloch主持设计。重新设计后的容器类库,提供了更严谨的实现,更丰富的类库。


容器类库中,分为两大类:Collection(集合)和Map(映像)。Collection存放的是一组各自独立的对象;Map存放的是一群成对的“键-值”对象。


在容器 API中,代表对象集合的接口有:
Collection:抽象的集合;
Set:Collection的子接口,一个无重复集;
List:Collection的子接口,一个有序可重复列表。

 

而Map接口有:
Map:一个用于放置“键-值”对的映像。

 

1 Collection接口


Collection是一个接口,它有三个基本的方法:
boolean add(Object obj):用于将一个对象放入到集合中,当原有Collection对象改变后,它返回true;
boolean addAll(Collection col):将另一个Collection对象中的所有元素插入该Collection对象中,当目标Collection对象改变后,它将返回true;


Iterator iterator():它返回一个Iterator(遍历器)接口对象

 

在Collection还定义了一些其他的方法,请参考API文档。

2 List

 

 

List是Collection的子接口,它用于保存有序的可重复的对象,它增加了对于索引的定义,放置在List内的对象被有序排列,可以通过一个整数索引来访问其中的对象,索引的值从0开始。List还重载了add()和addAll()方法,使得程序员可以通过索引值来指定插入对象在List中的位置:

 

 


add(int idx,Object obj):将指定对象obj插入到List中指定索引idx的位置,原来位于该位置的对象和其后的对象依次后移;
addAll(int idx,Collection col):将Collection参数col中的所有子对象依次插入到List中指定索引idx的位置,原来位于该位置的对象和其后的对象依次后移。


另外,在List中,还定义了一个方法set(int idx,Object obj),可以用来将指定索引idx的对象替换成obj,并返回被替换的对象。

 

有几个实现了这个接口的类:AbstractList, ArrayList, LinkedList和 Vector。在实际应用中,比较常用的是ArrayList和Vector。


可以将ArrayList想象成一种可以自动增加容量可以存放不同类型对象的数组(Array),它的使用方式非常简单,只需遵循如下步骤就可以了:
创建ArrayList实例;
调用ArrayList的add()方法将对象存入;
利用ArrayList的get()方法取得相应下标对应的对象,可以使用ArrayList的size()方法取得ArrayList的元素个数。


下面我们来看一个ArrayList的例子。

import java.util.*;

public class ListExample {
  public static void main(String[] args) {
    List list = new ArrayList();
    list.add("one");
    list.add("second");
    list.add("3rd");
    list.add(new Integer(4));
    list.add(new Float(5.0F));
    list.add("second");
    list.add(new Integer(4));
    System.out.println(list);
  }
}


List是一个有序的可重复集合,所以,在这边有几个重复的元素,也可以向list中加入。


程序运行结果如下:
[one, second, 3rd, 4, 5.0, second, 4]


可以看出,调用ArrayList对象list,它会返回它里面的所有对象。


注意:
  在集合中操纵的一定是对象,而不可能是简单类型数据,如,不能将简单类型数据直接放入到ArrayList中,而只能使用它对应的封装类对象。


  在Java SE 5中引入了自动装箱/拆箱的概念后,可以直接往容器中加入简单类型数据,但是,实际上并非是容器可以真正存放简单类型数据,而是从简单类型数据转换成相对应的封装类型对象这个动作,已经由自动装箱功能自动帮我们完成了,这点也需要提醒读者注意。

 

还有一个比较常用的List类是Vector,它和ArrayList非常类似,它们之间最大的区别在于,Vector是线程安全的,而ArrayList不是

 

因为这个原因,Vector在执行效率上会比ArrayList低,所以,如果你不用考虑多线程问题,请优先考虑ArrayList,否则,应该使用Vector。

 

在ArrayList和Vector中,采用的是数组的形式来保存对象的,这种方式将对象放在连续的位置中,它有一个很大的缺点是对它们进行删除或者插入操作的时候非常麻烦,例如,如果我们删除列表中的某个元素,那么,在这个元素之后的其他元素都必须向前挪动,而当往里插入数据的时候,在插入位置后的其他元素都必须往后挪动。(当然,作为Java程序员,我们可以不用考虑它的实现细节,但我们在选择这些列表的时候,我们应该知道它们的特性,并做出合适的选择。)

 

在JDK中还提供另外一种列表的实现,就是链接列表(LinkedList)。链接列表将每个对象放在独立的空间中,而且,在每个空间中还保存有下一个链接的索引,如果是双向链接列表,那么它还保存有上一个链接的索引,在Java中提供的是双向链接列表。另外,在LinkedList中不支持快速随机访问,如果想要访问LinkedList中的第n个元素,必须从头开始查找,然后跳过前面的n-1个元素。并且,虽然LinkedList提供了一个get()方法,可以根据指定的索引来获取对应的元素,但是,正因为它不支持快速随机访问,所以,它的效率比较低下,例如,我们来看下面的代码:


for(int j = 0;j<myLinkedList.size();j++)
{
 System.out.println(myLinkedList.get(j));
}


它的效率是非常低下的,每次循环读取一个元素时,都需要从myLinkedList的第一个元素重新搜索。

 

根据前面对ArrayList和LinkedList的说明,我们现在大致能够知道如何在在ArrayList和LinkedList之间作出取舍:如果你的列表需要快速的读取,但不经常进行元素的插入和删除操作,那么,应该选择ArrayList会好一些;而如果你需要对列表进行频繁的插入和删除操作,那么,应该选择LinkedList。


3 Iterator

 

 

 

遍历是指从集合中取出每一个元素的过程。

 

 


在Collection接口中,有一个iterator()方法,通过这个方法可以返回一个遍历器接口Iterator对象,通过这个对象,可以遍历Collection中的所有元素。在Iterator接口中,定义了一些用于从集合中取得元素的方法:
boolean hasNext()---判断在Iterator中是否还存在元素;
Object next()---使用这个方法从遍历器中取得元素;
remove()---从遍历器中删除集合。

 

通过Iterator,可以方便的遍历集合中的每一个元素。下面我们来看一个例子:


 

public class IteratorTest {
 public static void main(String[] args) {
  Book b1 = new Book("isbn-7-111", "Thinking in Java", "Bruce Eckel");
  Book b2 = new Book("isbn-7-5382", "The Moments", "Jimmy");
  Book b3 = new Book("isbn-7-5004", "Class", "Paul Fussell");
  Book b4 = new Book("isbn-7-5600", "The Scarlet Letter",
    "Nathaniel Hawthorne");
  Book b5 = new Book("isbn-7-5611", "The Portrait Of a Lady",
    "Henry James");
  Book b6 = new Book("isbn-7-302-06986-7",
        "Programming With J2ME","Alex Wen");

  ArrayList al = new ArrayList();
  al.add(b1);
  al.add(b2);
  al.add(b3);
  al.add(b4);
  al.add(b5);
  al.add(b6);

  Iterator iterator = al.iterator();
  while (iterator.hasNext()) {
   System.out.println((Book) iterator.next());
  }
 }

}

class Book {
 private String isbn, title, author;

 public Book(String theIsbn, String theTitle, String theAuthor) {
  isbn = theIsbn;
  title = theTitle;
  author = theAuthor;
 }

 public String getIsbn() {
  return isbn;
 }

 public String getTitle() {
  return title;
 }

 public String getAuthor() {
  return author;
 }

 public String toString() {
  return "[isbn=" + isbn + " ,title=" + title + " ,author=" + author
    + "]";
 }
}


这里定义了一个类Book用于测试。实例化6个Book对象放入到ArrayList对象al中,我们将在下一节详细论述ArrayList。然后,利用ArrayList的iterator()方法,可以得到一个Iterator对象,通过这个对象的hasNext()方法判断是否已经遍历完Iterator,用Iterator的next()方法取得Iterator中的元素,并将它打印出来。通过这种方式,可以将在集合中的对象全部取出来。在这边需要注意的是,next()方法返回的是Object对象,所以需要将它造型成Book对象。


编译运行,将得到如下的输出结果:
[isbn=isbn-7-111 ,title=Thinking in Java ,author=Bruce Eckel]
[isbn=isbn-7-5382 ,title=The Moments ,author=Jimmy]
[isbn=isbn-7-5004 ,title=Class ,author=Paul Fussell]
[isbn=isbn-7-5600 ,title=The Scarlet Letter ,author=Nathaniel Hawthorne]
[isbn=isbn-7-5611 ,title=The Portrait Of a Lady ,author=Henry James]
[isbn=isbn-7-302-06986-7 ,title=Programming With J2ME ,author=Alex Wen]


注意因为在类Book中覆盖了toString()方法,所以,打印Book对象将会打印出toString()方法中返回的字符串。

 

和Iterator相似的另一个用于遍历集合的接口是Enumeration(枚举器),这个接口通常用于遍历Vector对象中的元素,通过Vector的elements()方法,可以得到一个Enumeration对象,然后,就和Iterator一样,可以通过它的一些方法来将

 

Vector中的元素一个个的取出来:
boolean hasMoreElements():和Iterator中的hasNext()方法类似,用于判断集合中是否还有元素未被访问;
Object nextElement():和Iterator中的next()类似,用于取出下一个集合元素。

 

4 Set

 

 

 

Set接口和List接口不同的地方在于,它是不重复的对象集合。如果试图向Set中加入一个已经在它里面存在的对象,将不会成功。

 

 

Set有AbstractSet、 HashSet、 LinkedHashSet、 TreeSet这些子类。比较常用的是HashSet和TreeSet类,它可以从中快速查找指定的对象。


HashSet是一个无序的集,但是它能够快速的查找指定的对象,这是因为它采用了能够适应这个需求的散列码(hash code),我们在此不详细讨论散列码的基本概念,只需要知道,每个对象都有自己的散列码,在Object类中,有一个用于给对象产生散列码的方法hashCode(),它将会给每一个对象产生一个int的数据作为散列码,理想情形下,如果对象不是同一个对象,它们的散列码不会相同。

 

但是,或许你定义的类中,对于散列码的定义不是这样的,那么你也可以(而且经常也是必须)给自定义的类定义自己的hashCode()方法。而且,正如在前面所说的,对于自己定义的类,通常还需要定义自己的equals()方法。而如果你自己定义的类会用在HashSet、LinkedHashSet或者其他和散列码相关的地方,那么,你必须同时定义自己的hashCode()方法和equals()方法,并且这两个方法必须遵循如下的法则:对于MyObject类的实例x和y,如果x.equals(y),那么x.hashCode()必须和y.hashCode()相等。


从这个角度来看,我们在前面定义的类Citizen是不完整的:它没有覆盖hashCode()方法,下面我们来看如何给它定义自己的hashCode()方法:


public class Citizen {
 // 身份证号
 String id;

 // 其他属性略
 public Citizen(String theId) {
  id = theId;
 }

 // 覆盖equals()方法
 public boolean equals(Object obj) {
  // 首先需要判断o是否为null,
  // 如果为null,返回false
  if (obj == null) {
   return false;
  }
  // 判断测试的是否为同一个对象,
  // 如果是同一个对象,无庸置疑,它应该返回true
  if (this == obj) {
   return true;
  }
  // 判断它们的类型是否相等,
  // 如果不相等,则肯定返回false
  if (this.getClass() != obj.getClass()) {
   return false;
  }
  // 将参数中传入的对象造型为Citizen类型
  Citizen c = (Citizen) obj;
  // 只需比较两个对象的id属性是否一样,
  // 就可以得出这两个对象是否相等
  return id.equals(c.id);
 }

 public int hashCode() {
  return id.hashCode();
 }
}


因为String类已经覆盖了Object的hashCode()方法,而在这个Citizen类中,id可以作为Citizen对象的唯一标识符,所以,在这个例子中,我们将它的散列码作为Citizen的散列码。

 

对于hashCode()方法的定义,没有一个统一的规则,在定义它的时候,可以参考Joshua Bloch在《Effective Java》中提出的方法:


将一个非零常数, 保存在 int类型的 result 变量中;


对对象中每一个“主要属性” f (应该是说被方法 equals() 考虑的每一个属性), 进行以下计算:
  2.1 对这个属性计算出类型为 int 的 hash 值 c:
  2.1.1 如果这个属性是个 boolean, 计算 (f ? 0 : 1);
  2.1.2 如果这个属性是个 byte,char,short, 计算 (int)f;
  2.1.3 如果这个属性是个 long, 计算 (int)(f ^ (f >>> 32));
  2.1.4 如果这个属性是个 float, 计算 Float.floatToIntBits(f);
  2.1.5 如果这个属性是个 double, 计算 Double.doubleToLongBits(f), 然后将计算结果按步骤2.1.3 计算;
2.1.6 如果这个属性是个 object reference, 调用该对象的 hashCode(), 如果属性值是 null, 就传回 0;
2.1.7 如果属性是个 array, 请将每个元素视为独立属性, 对每个元素实行上述原则, 再依 2.2 的步骤将这些数值组合起来。
 2.2 将步骤 2.1 所计算出来的 hash 码 c 按照下列公式组合到变量 result 中:
result = 37*result + c;
返回result;
完成hashCode()方法定义后,测试相同的实例是否具有相同的hash码,如果不同,寻找原因,修改hashCode()定义。

 

下面我们来看一个HashSet的例子。


 

import java.util.*;

public class SetExample {
 public static void main(String[] args) {
  Set set = new HashSet();
  set.add("one");
  set.add("second");
  set.add("3rd");
  set.add(new Integer(4));
  set.add(new Float(5.0F));
  // 重复元素,将不会被加入
  set.add("second");
  // 重复元素,将不会被加入
  set.add(new Integer(4)); // 1
  System.out.println(set);
 }
}


HashSet是一个无序无重复的集,所以,如果试图向其中加入已经存在的对象,将不会成功,如在上面的//1中,将返回一个false,表示该对象没有被加入到HashSet中。


程序运行后的结果如下:
[one, 4, 5.0, 3rd, second]

 

除了HashSet,在Set的实现类中,还有一个比较典型的Set实现,就是TreeSet。TreeSet是一个有序无重复的集。我们可以用任何的顺序向TreeSet中加入对象,但是,当使用iterator()方法进行遍历时,我们可以得到一个有序的结果。例如,我们来看下面的例子:

import java.util.*;

public class SortedTreeSet {
 public static void main(String[] args) {
  TreeSet ts = new TreeSet();
  ts.add("Tom");
  ts.add("Jerry");
  ts.add("Mickey");
  ts.add("Donald");

  Iterator iterator = ts.iterator();
  while (iterator.hasNext())
   System.out.println(iterator.next());
 }
}


在这个程序中,我们定义了一个TreeSet,并且向它里面随机放进了四个String对象,然后我们使用遍历器将所有的元素遍历一次,并且将它打印出来,下面是它的运行结果:
Donald
Jerry
Mickey
Tom


可以看出,虽然我们往TreeSet对象插入数据的时候是无序的,但是,我们用遍历器去遍历它的时候,得到的结果是有序的。

 

上面的例子中,我们往TreeSet中加入了String对象,它会根据String的比较方式排序。但是,如果我们想把我们自己定义的类的对象放到TreeSet中,将会发生什么情况?它怎么对这些元素进行排序呢?例如,我们前面定义的Citizen类,我们没有给它定义任何的排序的算法,如果将它插入到TreeSet集中,将会发生什么情况呢?TreeSet又是如何对它们进行排序呢?


如果我们往TreeSet集中插入相互之间不能进行比较的对象,那么,将会发生ClassCastException异常。所以,在TreeSet中,只能用于保存可以相互进行比较大小的对象。

现在的问题是,如何让自定义的类的对象可以比较?答案是,让你的类实现Comparable接口,并且实现这个接口的唯一的方法compareTo(),它接受一个Object对象,你可以在这个方法中定义对象的排序规则,它应该返回一个整数值,根据这个值的是大于0、小于0还是等于0可以确定两个对象的顺序。String类就是因为实现了这个Comparable接口,才可以对它进行比较。


下面我们还是来看让前面的Citizen类成为“可比较”对象:


 

public class Citizen implements Comparable {
 // 身份证号
 private String id;

 // 其他属性略
 public Citizen(String theId) {
  id = theId;
 }

 // 覆盖equals()方法
 public boolean equals(Object obj) {
  if (obj == null) {
   return false;
  }
  if (this == obj) {
   return true;
  }
  if (this.getClass() != obj.getClass()) {
   return false;
  }
  Citizen c = (Citizen) obj;
  return id.equals(c.id);
 }

 // 覆盖hashCode()方法
 public int hashCode() {
  return id.hashCode();
 }

 // 覆盖toString()方法
 public String toString() {
  return id;
 }

 public String getId() {
  return id;
 }

 // 定义compareTo方法
 public int compareTo(Object other) {
  Citizen c = (Citizen) other;
  return id.compareTo(c.getId());
 }

}


我们让这个自定义的Citizen类实现Comparable接口,并且实现了它的compareTo()方法:首先将参数的Object对象造型为Citizen,然后,用对象本身的属性id和参数中的Citizen对象的getId()方法得到的id进行比较,因为它们都是String类型的,所以在此直接调用String类的compareTo()方法来进行比较就可以了,也就是说,两个Citizen的比较是通过它们的String类型的id属性来比较的。


然后我们就可以将这个Citizen对象加入到TreeSet中了。

 

public class SortedTreeSet {
 public static void main(String[] args) {
  TreeSet ts = new TreeSet();
  Citizen c1 = new Citizen("a10");
  Citizen c2 = new Citizen("a02");
  Citizen c3 = new Citizen("a99");
  ts.add(c1);
  ts.add(c2);
  ts.add(c3);

  Iterator iterator = ts.iterator();
  while (iterator.hasNext())
   System.out.println(iterator.next());
 }
}


运行这个程序将会输出如下的结果:
a02
a10
a99


可以看到,输出的结果是有序的。


提示:
 虽然没有规定往TreeSet中插入的元素必须是“同类”的,但是,因为它会对插入其中的对象进行比较,所以,通常,我们往TreeSet中插入的元素是相同类型的对象,或者相互之间有继承关系的类的对象。