黑马程序员——集合框架2:List集合

------- android培训java培训、期待与您交流! ----------

       在上一篇博客中,我们简单介绍了集合框架的意义、优点、成员结构,以及所有集合类的共性方法。从这一篇博客开始,我们将分别介绍Collection接口下的常用子接口及其实现类各自的特点。首先介绍List集合,然后介绍Set集合,这两种集合是实际开发中较为常见的两种。

1.     List集合特点

list集合类最大的两个特点是:

(1)    元素是有序的;

(2)    元素可以重复。

这里指的有序是说,存入元素和取出元素的顺序相同。之所以list集合类拥有上述两个特点,是因为list集合为每个元素定义了索引。这与数组的定义时类似的,不同索引处可以存储相同的元素,并且的元素是按照存储顺序一次向下排列的。相对于List集合的这两大特点,Set集合完全相反:元素不仅是无序的,并且不可以重复。

2.     List集合共性方法

List接口中也包含了前述Collection接口中的共性方法,这里不再赘述。我们主要介绍List集合的一些特有方法。

2.1    方法简介

添加:

void add(int index,E element):将指定对象element添加到在List集合中的指定位置。index表示添加位置索引,或称为角标值。
boolean addAll(int index,Collection<? extends E> c):向该集合中,在指定位置,添加指定集合c中的所有元素。

 

获取:

E get(int index):获取指定角标位上的元素。也就是说所有实现List接口的集合类,除了可以通过迭代器获取元素,还可以通过get方法获取元素。这是List集合,与其他集合类的一大区别。

int indexOf(Objecto):获取到指定元素在该集合中第一次出现的角标值。

intlastIndexOf(Object o):获取指定元素在该集合中最后一次出现的角标值。

ListIterator<E>listIterator():获取到List集合的特有迭代器对象——ListIterator。ListIterator是实现了Iterator接口的内部接口。该迭代器对象初始化时,内部指针指向第一个元素。

ListIterator<E>listIterator(int index):上述方法的重载方法,不同点是迭代器对象初始化时,内部指针指向指定角标值对应的元素。

List<E>subList(int fromIndex, int toIndex):将指定角标区间内的元素存储到一个新的List集合对象内,并返回新的集合。注意,同样是包含头,不包含尾。

 

删除:

boolean remove(intindex):删除指定角标位上的元素。

 

替换:

E set(int index, Eelement):将该集合中指定角标位上的元素,替换为指定元素,并返回被替换的元素对象。

2.2    方法演示

2.2.1 一般方法

添加:

代码1:

import java.util.*;
 
class ListDemo
{
	public static void main(String[] args)
	{
		ArrayList al1 = new ArrayList();
 
		//同样为了演示方便,仅添加字符串对象
		al1.add("String1");
		al1.add("String2");
		al1.add("String3");
		System.out.println("原集合:");
		System.out.println(al1);
		System.out.println();
             
		//在指定位置插入元素
		al1.add(0,"String0");
		System.out.println("插入单个元素:"+al1);
 
		//在指定位置添加多个元素
		ArrayListal2 = new ArrayList();
		al2.add(newInteger(1));
		al2.add(newInteger(2));
		al2.add(newInteger(3));
 
		al1.add(0,al2);
		System.out.println("插入多个元素:"+al1);
	}
}
运行结果为:

原集合:

[String1, String2, String3]

 

插入单个元素:[String0, String1, String2,String3]

插入多个元素:[[1, 2, 3], String0, String1,String2, String3]

最后一行结果显示,当某一部分元素是从其他集合中添加进来时,将会把这一部分数据用方括号括起来,以示区分。

获取:

代码2:

import java.util.*;
 
class ListDemo2
{
	public static void main(String[] args)
	{
		ArrayList al = new ArrayList();
		al.add("String1");
		al.add("String2");
		al.add("String3");
 
		//获取指定角标位上的元素
		System.out.println("获取2号角标上的元素:"+al.get(2));
 
		//通过for循环加get方法获取到所有的元素
		System.out.println("\n通过for循环加get方法获取到所有元素:");
		for(int x=0; x<al.size(); x++)
		{
			System.out.println("al("+x+")= "+al.get(x));
		}
 
		//通过迭代器获取到所有元素
		System.out.println("\n通过迭代器获取到所有元素:");
		for(Iterator it = al.iterator(); it.hasNext(); )
		{
			System.out.println(it.next());
		}
 
		//获取到指定元素的角标值
		System.out.println("\nString3的角标值:"+al.indexOf("String3"));
 
		//获取指定角标区间的子集合
		List list = al.subList(1, 4);
		System.out.println("\n子集合长度:"+list.size());
		System.out.println("子集合中元素:"+list);
	}
}
运行结果为:

获取2号角标上的元素:String3

 

通过for循环加get方法获取到所有元素:

al(0) = String1

al(1) = String2

al(2) = String3

 

通过迭代器获取到所有元素:

String1

String2

String3

 

String3的角标值:2

 

子集合长度:3

子集合中元素:[String2, String3, String4]

注意:subList方法的操作原理同样遵循,包含头不包含尾的特性。

 

删除与替换:

代码3:

import java.util.*;
 
class ListDemo3
{
	public static void main(String[] args)
	{
		ArrayList al = new ArrayList();
		al.add("String1");
		al.add("String2");
		al.add("String3");
 
		System.out.println("原集合:"+al);
 
		//删除指定位置上的元素
		al.remove(1);
		System.out.println("删除:"+al);
 
		//替换指定位置上的元素为指定元素
		al.set(1,"String2");
		System.out.println("替换:"+al);
	}
}
运行结果为:

原集合:[String1, String2, String3]

删除:[String1, String3]

替换:[String1, String2]

 

       我们在下面演示listIterator迭代器之前,先演示Iterator迭代器的方法,以反映其功能的不足。

Iterator迭代器:

需求:在通过迭代器获取元素过程中(或者简称迭代过程中),添加或者删除元素。

代码:

代码4:

import java.util.*;
 
class ListDemo4
{
	public static void main(String[] args)
	{
		ArrayList al = new ArrayList();
		al.add("String1");
		al.add("String2");
		al.add("String3");
 
		//在迭代过程中,添加或者删除元素
		Iterator it = al.iterator();
		while(it.hasNext())
		{
			//存如ArrayList集合中的对象,都将以Object对象返回
			Object obj = it.next();
			if(obj.equals("String2"))
				al.add("String4");
 
			System.out.println(obj);
		}
	}
}
运行结果为:

String1

String2

Exception in thread "main"java.util.ConcurrentModificationException

       at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:886)

       at java.util.ArrayList$Itr.next(ArrayList.java:836)

       at test10.ListDemo.main(ListDemo.java:19)

       结果显示抛出了ConcurrentModificationException异常,该异常类的API文档(该类在Java标准类库java.util包内)描述为:当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。意思是,当我们通过迭代器获取集合中元素的同时,又通过集合对象本身获取元素的方法访问元素,就可能会导致安全问题。比如,通过迭代器删掉了某个元素,但又通过get方法去访问该元素,就会造成矛盾,这就是所谓的并发修改。

       因此上述问题的解决方法就是,要么就通过迭代器访问元素,要么就通过集合本身的方法访问元素,不要同时操作。如果需要在迭代的同时删除某个元素时,可以使用迭代器的remove方法。

代码:将代码4做一简单修改

代码5:

import java.util.*;
 
class ListDemo5
{
	public static void main(String[] args)
	{
		ArrayList al = new ArrayList();
		al.add("String1");
		al.add("String2");
		al.add("String3");
 
		//打印改动前的集合
		System.out.println(al);
		//在迭代过程中,添加或者删除元素
		Iterator it = al.iterator();
		while(it.hasNext())
		{
			//存如ArrayList集合中的对象,都将以Object对象返回
			Object obj = it.next();
			if(obj.equals("String2"))
				it.remove();
                    
			System.out.println(obj);
		}
		//打印改动后的集合
		System.out.println(al);
	}
}
运行结果为:

[String1, String2, String3]

String1

String2

String3

[String1, String3]

       大家会发现,在迭代过程中虽然将String2删除了,但是还是打印在了控制台。这是因为,remove方法仅仅是将指向String2对象的引用从集合中删除了,但是该对象本身还是存在于堆内存中的,并且又有了新的引用obj指向该对象,所以是可以被打印的,但是从最后一行结果来看,String2确实不存在于集合中了。从上述代码的演示结果来看,Iterator迭代的方法确实是有限的,只能进行判断、获取和删除,因此为了增强迭代器的方法,针对List集合,定义了Iterator迭代器的子类——listIterator。

2.2.2 通过迭代器实现集合元素的增删改查——列表迭代器

ListIterator迭代器:

通过查阅该迭代器的API文档,可知listIterator也是接口,并且是Iterator的子接口,其中值得我们注意的是:该迭代器内部的指针标位置始终位于两个元素之间,而并不指向任何一个元素,因此长度为n的列表的迭代器有n+1个可能的指针位置。

该ListIterator迭代器包含如下方法:

void add(E e):添加指定元素到该集合中,迭代器指针当前指向的角标位。

boolean hasNext():正向遍历该集合,判断集合中是否还有元素。

booleanhasPrevious():反向遍历该集合,判断集合中是否还有元素。

E next():返回集合中的下一个元素。

int nextIndex():获取下一个元素的角标。

E previous():返回前一个元素。

int previousIndex():获取前一个元素的角标。

void remove():从列表中删除通过next或者previous方法获取的元素。

void set(E e):将通过next或者previous方法获取的元素,替换为指定元素。

上述这些方法,表明listIterator迭代器有着比Iterator迭代器更为丰富的方法,对元素的操作更为灵活。我们通过下面的几段代码来演示上述方法。

代码:

代码6:

import java.util.*;
 
class ListDemo6
{
	public static void main(String[] args)
	{
		ArrayList al = new ArrayList();
		al.add("String1");
		al.add("String2");
		al.add("String3");
 
		//打印原集合内容
		System.out.println(al);
 
		ListIterator li = al.listIterator();
		while(li.hasNext())
		{
			Object obj = li.next();
			if(obj.equals("String2"))
				//在String2后面添加指定String4
				li.add("String4");
			elseif(obj.equals("String3"))
				//将String3替换为String5
				li.set("String5");
		}
 
		//打印改动后集合的内容
		System.out.println(al);
	}
}
运行结果为:

[String1, String2, String3]

[String1, String2, String4, String5]

在String2后面添加了String4,并将String3替换为String5。

这里需要注意的是,add方法是将指定元素插入到指针指向的元素后面,而不是集合的末尾,就像代码6的结果显示的那样,获取到String2以后,就将add方法的参数插入到String2后面的位置。

 

代码7:

import java.util.*;
 
class ListDemo7
{
	public static void main(String[] args)
	{
		ArrayList al = new ArrayList();
		al.add("String1");
		al.add("String2");
		al.add("String3");
 
		ListIterator li = al.listIterator();
 
		//未进行迭代时,判断指针指向位置(0号元素前)前有没有元素
		System.out.println(li.hasPrevious());
 
		while(li.hasNext())
		{
			System.out.println(li.next());
		}
 
		//将集合遍历一遍以后,判断指针指向位置(末尾元素位置后)后有没有元素
		System.out.println(li.hasPrevious());
 
		System.out.println("==============================================");
 
		//再反向遍历一遍集合
		while(li.hasPrevious())
		{
			System.out.println(li.previous());
		}
 
		//将集合反向遍历一遍以后,判断指针指向位置
		//(此时,再次指向头元素前面的位置)前后有没有元素
		System.out.println("hasPrevious():"+li.hasPrevious());
		System.out.println("haxNext():"+li.hasNext());
	}
}

运行结果为:

hasPrevious():false

haxNext():true

String1

String2

String3

hasPrevious():true

haxNext():false

============================================

String3

String2

String1

hasPrevious():false

haxNext():true

       运行结果也印证了API文档中,关于迭代器内部指针的指向问题,迭代开始前指针指向第一个元素前面的位置,因此hasPrevious方法返回false,相反hasNext方法返回true;当迭代完毕以后,指针指向末尾元素后面的位置,因此hasPrevious方法返回true,相反hasNext方法返回true,表示指针指向位置前面是有元素的(最后一个元素)。

3.     List集合子类介绍

       在上面的内容中,我们简单介绍了List集合类的共性方法,下面我们来分别介绍List集合类三种常见子类——ArrayList、LinkedList和Vector,底层实现的数据结构及其特有方法。

3.1    ArrayList

ArrayList集合底层的数据结构是数组结构。数组结构最大的特点就是每个元素拥有对应的角标,通过指定角标就可以最快的速度访问某个元素,换句话说,数组结构的查询速度是最快的。但是,数组结构的弊端在于,当我们在头角标位插入(或删除)指定元素时,所有元素都将向后(或向前)移动一位,然后将添加的元素(或删除的元素)置于空出来的头角标位,当然这是最坏的情况,但即使插入到其他位置,增删速度也是比较慢的,并且元素数量越多,增删速度越慢。

总结起来就是,ArrayList集合类,查询速度快,但是增删元素较慢。

       我们在之前的内容中,详细介绍过该类的大部分方法,因此这里就不再赘述。

3.2    LinkedList

3.2.1 LinkedList集合数据结构简介

LinkedList集合类使用的是链表数据结构。这里我们引用《数据结构与算法分析 Java语言描述第二版》中对链表结构的解释:链表由一系列节点组成,这些节点之间不必在内存地址中相连。每一个节点包含一个元素和指向后继元素的(link),我们称之为next链。最后一个next链的引用为null。下图表示了一个简单链表的结构,


图1 链表数据结构

查找元素:

例如查找A2元素时,就需要从A0元素开始判断,如果不是指定元素,就通过next链判断A0指向的A1元素,就这样一直向后查找,直到找到指定元素为止。如果我们恰好查找的是链表末尾节点上的元素,那么就需要从头至尾全部搜索一遍,因此查找效率比较低。

增删元素:

当我们需要向链表数据结构中添加元素时,比如在A1后面添加A4元素时,首先将A1原先指向A2的next链指向A4然后,再将A4的next链指向A2即可,就像下图所示 

 

图2 添加元素

而需要将链表中的某个元素删除时,比如删除上表中的A4,只需将A2指向A4的next链直接指向A2即可,就像下图所示


图3 删除元素

因此,链表数据结构在增删元素时是不需要像数组结构那样大面积的移动元素的位置的,因此链表结构增删元素的速度相比数组结构快很多。

       总结起来就是,LinkedList集合类,查询速度慢,但增删速度快。

3.3.2 LinkedList集合类特有方法简介

既然LinkedList集合类实现了List接口,那么该类也就继承了Collection和List的所有共性方法,因此这里我们只介绍LinkedList的一些特有方法。

添加:

void addFirst(E e):将指定元素添加到该集合的开头。

void addLast(E e):将指定元素添加到该集合的结尾。

boolean offer(E e):将指定元素添加到该集合的尾部,添加成功,返回true;否则,返回false。

boolean offerFirst(Ee):将指定对象添加到该集合的开头,添加成功,返回true;否则,返回false。

boolean offerLast(Ee):将指定对象添加到该集合的结尾,添加成功,返回true;否则,返回false。

上述三个offer系列方法在1.5版本后替代了add系列方法,可以判断是否添加成功,建议实际开发使用。

删除:

E removeFirst():获取并删除该集合的第一个元素。如果集合为空,则抛出NoSuchElementExceptin异常。

E removeLast():获取并删除该集合的最后一个元素。如果集合为空,则抛出NoSuchElementException异常。

E poll():获取并移除该集合的第一个元素;如果集合为空,则返回null。

E pollFirst():获取并移除该集合的第一个元素;如果集合为空,则返回null。

E pollLast():获取并移除该集合的最后一个元素;如果集合为空,则返回null。

以上三个poll系类方法同样也是1.5版本以后定义的方法,当集合为空时不再抛出NoSuchElementException异常,建议实际开发中使用。

获取元素:

E getFirst():获取该集合的第一个元素,如果集合为空,抛出NoSuchElementException异常。

E getLast():获取该集合的最后一个元素。如果集合为空,抛出NoSuchElementException异常。

E peek():获取但不移除该集合的第一个元素。如果集合为空,返回null。

E peekFirst():获取但不移除该集合的第一个元素。如果集合为空,返回null。

E peekLast():获取但不移除该集合的第一个元素。如果集合为空,返回null。

以上三个方法与poll系列方法一样,是1.5版本以后定义的,集合为空时不再抛出NoSuchElementException异常,建议实际开发使用。

 

下面我们通过下面的代码来演示上述方法的使用及效果,

添加:

代码8:

import java.util.*;
 
class LinkedListDemo
{
	public static void main(String[] args)
	{
		LinkedList link = new LinkedList();
 
		//将每个元素都添加到该集合的头部
		link.offerFirst("String1");
		link.offerFirst("String2");
		link.offerFirst("String3");
 
		//打印该集合,观察元素在集合中的排列情况
		System.out.println(link);
	}
}
运行结果为:

[String3, String2, String1]

由于每个新元素都添加到了集合的开头,因此首先添加的元素的位置在尾部,而最后添加的元素的位置在开头。offerLast方法同理,先添加的在集合开头,后添加在集合结尾,不再演示了。

 

获取:

代码9:

import java.util.*;
 
class LinkedListDemo2
{
	public static void main(String[] args)
	{
		LinkedList link = new LinkedList();
 
		link.offer("String1");
		link.offer("String2");
		link.offer("String3");
 
		//获取第一个元素
		System.out.println(link.peekFirst());
		//获取最后一个元素
		System.out.println(link.peekLast());
		System.out.println("======================================");
 
		//不通过迭代器获取到集合中的所有元素
		//通过removeFirst方法,在删除的同时获取到元素,直到集合为空
		while(!link.isEmpty())
		{
			System.out.println(link.pollFirst());//也可以通过pollLast方法,逆向获取元素
		}
	}
}
运行结果为:

String1

String3

======================================

String1

String2

String3

删除:

代码10:

import java.util.*;
 
class LinkedListDemo3
{
	public static void main(String[] args)
	{
		LinkedList link = new LinkedList();
 
		link.offer("String1");
		link.offer ("String2");
		link.offer ("String3");
 
		System.out.println("集合长度为:"+link.size());
 
		//删除第一元素,同时返回被删除的元素
		System.out.println(link.pollFirst());
		//删除最后一个元素,同时返回被删除元素
		System.out.println(link.pollLast());
 
		System.out.println("集合长度为:"+link.size());
	}
}
运行结尾为:

集合长度为:3

String1

String3

集合长度为:1

那么从这里我们就能区分出peekFirst、peekLast方法与pollFirst、pollLast之间的区别:前者仅用于获取元素;后者,在获取元素的同时删除元素。

       我们这里仅演示了1.5版本后定义的新方法,旧有方法的调用方式及结果基本相同,不再演示。

3.3    实际开发的选择

ArrayList和LinkedList这两个集合是实际开发中比较常用的集合,那么在实际开发中如何选择呢?根据他们俩的数据结构特点,

(1)    如果需要对集合进行频繁的存取操作,那么就使用LinkedList;

(2)    如果涉及到存取但是并不非常频繁,既可以选择ArrayList,也可以选择LinkedList;

(3)    如果同时需要增删和查询操作,建议使用ArrayList。这是因为,频繁的增删操作并不常见,更多的情况是较为频繁的查询。并且,在元素数量并不非常多的情况下,其增删元素的速度也是可以接受的。

3.4    Vector

(1)    Vector与ArrayList的区别

Vector集合类底层使用的数据结构与ArrayList是相同的,也是数组结构,并且实际上Vector集合类的功能与ArrayList集合类几乎完全相同,那么他们的区别是什么呢?

第一点区别是,两者的出现版本不同,通过查阅ArrayList和Vector集合类的API文档可知,ArrayList是在JDK1.2版本时出现的,而Vector集合类是在JDK1.0版本时就已经存在了。换句话说,Vector类是在集合框架出现以前就存在了。

第二点区别是,Vector集合类是线程同步的,而ArrayList集合类的API文档使用了黑体字强调,该集合类是线程不同步的,因此,Vector集合类无论添加、删除还是查询速度都是非常慢的,因此,实际开发时建议大家使用ArrayList。如果需要同步线程,可以选择手动加锁,也可以使用集合框架提供给我们的线程安全解决方法(后面会讲到)。

第三点区别,集合作为容器相比数组最大的特点在于,可变长度。而所谓可变长度,其实底层还是在使用固定长度的数组,只是在添加的元素数量超过长度以后,会创建一个比原来数组长度更大的新数组,然后再将元素存储到新数组中。ArrayList集合类空参数构造函数的API文档告诉我们:构造一个初始容量为10的空列表。而当添加元素数量超过10以后,新集合的长度将提高50%,而Vector集合类的长度提高幅度为100%。两相比较,ArrayList更有利于优化内存空间,不仅可以实现可变长度,还可以节约内存。

(2)    Vector方法简介与演示

Vector集合类在JDK1.2版本成为集合框架的一员以后,它也继承了很多来自Collection和List接口的共性方法,这里就不再赘述了,而从Vector类API文档方法摘要中我们可以看到该类在1.2版本以前的一些特有方法,比如addElement,添加元素;elementAt,返回指定角标处的元素等等方法,只要方法名中包含Element或者element,就是该类早期版本的特有方法,这些方法的缺点就是方法名冗长,不利于代码书写和阅读。

在这些特有方法中,我们只重点介绍其中一个

Enumeration<E>elements():返回此集合的枚举。该方法的返回值类型为Enumeration,我们继续查阅Enumeration的API文档,它是一个接口,我们称之为枚举。该接口只包含两个方法——hasMoreElements和nextElement方法,从方法名来看,与Iterator是非常类似的,而实际上两者的作用也是相同的,都是用于获取集合中的元素。我们通过下面的代码来演示该接口的方法,

代码:

代码11:

import java.util.*;
 
class VectorDemo
{
	public static void main(String[] args)
	{
		Vector v = new Vector();
 
		//为了演示方便依然添加字符串,作为元素
		v.add("String1");
		v.add("String2");
		v.add("String3");
 
		//获取枚举对象,作为迭代器
		Enumeration en = v.elements();
		while(en.hasMoreElements())
		{
			//获取元素,并打印
			System.out.println(en.nextElement());
		}
	}
}
运行结果为:

String1

String2

String3

从运行结果来看,Enumeration与Iterator迭代器无论在使用方法,还是结果都是相同的。那么实际上,枚举是在JDK1.2版本以前,Vector集合特有的元素获取方式,那么至此,Vector集合类,便具有了三种获取元素的方法——通过get(或elementAt)方法集合for循环、Iterator迭代器和Enumeration枚举(也算是一种迭代器)。

       那么在实际开发中,我们应该使用哪种迭代器呢?Enumeration接口的API文档告诉我们:此接口的功能与Iterator接口的功能是重复的。但是Iterator相比Enumeration有两个优点:(1) Iterator接口添加了一个可选的移除操作;(2) 并使用较短的方法名。因此,新的实现应该优先考虑使用Iterator接口而不是Enumeration接口,所以我们的选择也是显然的。

       而我们之所以还要这里提到枚举的原因是,后面我们将要介绍的IO技术中,会再次涉及到枚举,因此这里我们事先介绍一下。

       最后,如果面试时,被问到ArrayList和Vector的区别,那么他们之间最大区别就在于是否具备枚举。

4.     List集合练习

4.1    LinkedList练习

需求:通过LinkdList集合,模拟堆栈和队列数据结构。

分析:

堆栈和队列是两种结构比较简单的数据结构。堆栈数据结构的特点是,先存储的数据,位于集合的末尾,而最后存储的数据,位于集合的开头,就好比是一个水杯。队列结构正好相反,先存储的数据,位于集合开头,而后存储的数据,位于集合结尾,就好比是一个水管。因此总结两者的特点是,

堆栈数据结构:先进后出。

队列数据结构:先进先出。

实现方式:

队列:

定义一个名为Queue(意为队列)的类,定义一个LinkedList类型的成员变量,在构造函数内为该变量初始化一个LinkedList对象,该类功能的实现也是基于LinkedList对象的。分别定义存储元素方法——add,和获取元素方法——get。add方法基于LinkedList对象的offerLast方法,将每个新添加的元素置于集合末尾(也可使用offerFirst,只是获取元素时的顺序相反),最终的结果就是,最先添加的元素被“挤”到了集合的开头。而get方法调用LinkedList对象的pollFirst方法从头获取元素,即可实现先进先出。

堆栈:

       定义类名为Stack(意为堆栈),实现方式与队列基本类似。但是,如果add方法通过offerLast实现,那么get方法就通过pollLast即可,反之亦然。

代码:

代码12:

import java.util.*;
 
//队列:先进先出
class MyQueue
{
	//因为该类的功能实现基于LinkedList,因此定义一个LinkedList成员变量
	private LinkedList link;
 
	MyQueue()
	{
		//初始化一个LinkedList对象
		link = new LinkedList();
	}
 
	public void add(Object obj)
	{
		//将每个元素添加至集合的末尾
		link.offerLast(obj);
	}
	public Object get()
	{
		//先存储的元素被“挤”到集合开头,因此从头获取元素
		return link.pollFirst();
	}
	public boolean isEmpty()
	{
		return link.isEmpty();
	}
}
//堆栈:先进后出
class MyStack
{
	private LinkedList link;
	MyStack()
	{
		link = new LinkedList();
	}
	public void add(Object obj)
	{
		link.offerLast(obj);
	}
	public Object get()
	{
		//依次从末尾获取元素,实现先进后出
		return link.pollLast();
	}
	public boolean isEmpty()
	{
		return link.isEmpty();
	}
}
class LinkedListTest
{
	public static void main(String[] args)
	{
		//队列演示
		MyQueue mq = new MyQueue();
		mq.add("String1");
		mq.add("String2");
		mq.add("String3");
 
		while(!mq.isEmpty())
		{
			System.out.println(mq.get());
		}
 
		System.out.println("==================================");
 
		//堆栈演示
		MyStack ms = new MyStack();
		ms.add("String1");
		ms.add("String2");
		ms.add("String3");
 
		while(!ms.isEmpty())
		{
			System.out.println(ms.get());
		}
	}
}
运行结果为:

String1

String2

String3

==================================

String3

String2

String1

       从结果来看,MyQueue和MyStack分别实现了队列的“先进先出”功能,和堆栈的“先进后出”功能。类似上述代码体现的实现方式,在实际开发中比较常见,将Java中已有类的功能封装起来,并对外提供相对本项目更有针对性的方法,便于提高开发效率。

4.2    ArrayList集合练习

练习1:

需求:去除ArrayList集合中的重复元素。

分析:

       假设现有一个ArrayList集合,内含若干个元素,其中有些元素是重复的。实现该功能的思想是:首先创建一个临时容器(另一个ArrayList集合),然后将原有集合内的元素遍历一遍,遍历的同时判断这些元素是否包含于临时容器中,如果不包含,则将这些元素添加到临时容器中;反之,不添加。

代码:

代码2:

import java.util.*;
 
class ArrayListTest
{
	public static void main(String[] args)
	{
		ArrayList al = new ArrayList();
 
		//人为添加一些重复元素
		al.add("String1");
		al.add("String1");
		al.add("String2");
		al.add("String2");
		al.add("String3");
		al.add("String4");
 
		//打印原集合中所有元素
		System.out.println(al);
		//去掉元集合中的重复元素
		al = singleElement(al);
		//打印去重以后集合中的元素
		System.out.println(al);
	}
	public static ArrayListsingleElement(ArrayListal)
	{
		//定义一个临时容器
		ArrayList temp = new ArrayList();
 
		//遍历原集合中的所有元素
		for(Iterator it = al.iterator();it.hasNext(); )
		{
			Object obj = it.next();
 
			//如果原集合中的元素不包含于临时集合中,则添加该元素
			if(!temp.contains(obj))
				temp.add(obj);
		}
 
		//返回临时集合
		return temp;
	}
}
运行结果为:

[String1,String1, String2, String2, String3, String4]

[String1,String2, String3, String4]

结果显示,该方法确实将原集合中的重复元素去掉了。

       针对该练习,需要强调的是:List集合的contains方法在判断某个元素是否包含于该集合中时,会遍历集合中的元素,在遍历的同时通过equals方法判断传入的对象是否与集合中的元素相同。而由于String类复写了Object类的equals方法,判断其内容而非地址,所以,两个字符串只要内容不同,那么就认为这两个字符串对象就是不同的对象(不过,其他集合类contains的实现方式却并非都是如此,需要注意)。但是,这并不适用于其他所有类的对象,如果集合中存储的元素类型为自定义类型(例如Person类),此时需手动复写equals,自定义如何判断两元素是否相同的方法,否则就会由于对象地址不同,而无法去除我们认为“内容重复”,而地址不同的元素了。下面我们就通过练习二来实现去除ArrayList集合中重复的自定义元素。

练习二:

需求:去除ArrayList集合中重复的自定义元素。以Person类为例。

分析:

       当两Person对象的姓名和年龄相同时,就判定为同一个Person对象。此时,就需要在定义Person的时候,复写Object类的equals方法,当传入的Person对象姓名与年龄与该对象(this所指向的对象)姓名年龄均相同时,返回true;否则,返回false。

实现方式:

想要实现上述需求,需要解决下面2个问题:

问题一:为了显示去除重复元素的效果,需要定义一个用于打印集合中元素的方法——printElement,在该方法中,向控制台打印Person对象的姓名和年龄。但是当我们向ArrayList集合中存入Person对象时,add方法是将所传参数对象(Person对象)多态地作为Object对象接收的,这里就隐式的进行了向上转型动作,因此通过迭代器获取的元素类型同样也是Object类型,如果想要通过getName方法和getAge方法(Person类的特有方法),获取Person对象的信息,就必须要经过一步向下转型,将Object类型对象强制转换为Person类型。

问题二:如何去复写equals方法?如果像下面的代码这样定义equals方法是不可行的:

public boolean equals(Personp)

由于,我们操作的对象类型均为Person,因此可能会下意识的将equals方法的参数列表类型定义为Person,然而Object类中的equals方法的参数列表类型为Object,因此上述代码并没有起到复写的作用,这就导致在调用contains方法时依然调用从Object类继承来的原始equals方法,而起不到去重的效果。这里主要强调的是复写需要注意的问题——若要实现复写,需要子父类方法的方法名、参数列表及返回值类型完全相同。

代码:

代码3:

import java.util.*;
 
//自定义Person类
classPerson
{
	private String name;
	private int age;
	Person(String name, int age)
	{
		this.name = name;
		this.age = age;
	}
 
	public String getName()
	{
		return name;
	}
	public int getAge()
	{
		return age;
	}
 
	//复写Object类的equals方法,保证方法名、参数列表和返回值类型全部相同
	public boolean equals(Object obj)
	{
		//若要调用Person对象的getName和getAge方法,需进行向下转型
		Person p = (Person)obj;
		//当姓名和年龄均相同时,才认为两Person对象为同一个对象
		return p.getName().equals(this.name)&&p.getAge() == this.age;
	}
}
class ArrayListTest2
{
	public static void main(String[] args)
	{
		ArrayList al = new ArrayList();
 
		al.add(newPerson("Jack", 23));
		al.add(newPerson("Peter", 31));
		al.add(new Person("Lucy",19));
		al.add(newPerson("Jack", 23));
		al.add(newPerson("Lucy", 19));
		printElements(al);
 
		System.out.println("================");
 
		al = singleElement(al);
		printElements(al);
	}
	public static void printElements(Listlist)
	{
		for(Iterator it = list.iterator();it.hasNext(); )
		{
			//调用getName和getAge方法,也要先进行向下转型
			Person p =(Person)it.next();
			System.out.println("name="+p.getName()+", age= "+p.getAge());
		}
	 }
	public static ArrayList singleElement(Listlist)
	{
		ArrayList temp = new ArrayList();
 
		for(Iterator it = list.iterator();it.hasNext(); )
		{
			/*
				这里不需要进行向下转型
				因为,equals方法是继承并复写了Object类的equals方法
				而obj对象是由Person对象向上转型而来
				当调用obj的equals方法时(contains方法底层调用)
				由多态的特点决定了
				执行的还是子类Person的equals方法
			*/
			Objectobj = it.next();
			if(!temp.contains(obj))
				temp.add(obj);
		}
 
		return temp;
	}
}
执行结果为:

name=Jack, age= 23

name=Peter, age= 31

name=Lucy, age= 19

name=Jack, age= 23

name=Lucy, age= 19

================

name=Jack, age= 23

name=Peter, age= 31

name=Lucy, age= 19

成功去掉了集合中的重复Person对象。

为了解释contains底层的运行过程,我们在Person类equals方法第一和第二两个语句之间,插入以下一行语句:

System.out.println(this.name+"......"+p.getName());

这一语句的作用就是,一旦有对象调用了equals方法判断与传入对象是否相同,就会先后打印该对象姓名和参数对象的姓名。再次执行后的效果为:

name=Jack, age= 23

name=Peter, age= 31

name=Lucy, age= 19

name=Jack, age= 23

name=Lucy, age= 19

================

Peter......Jack

Lucy......Jack

Lucy......Peter

Jack......Jack

Lucy......Jack

Lucy......Peter

Lucy......Lucy

name=Jack, age= 23

name=Peter, age= 31

name=Lucy, age= 19

这里我们主要关注“================”分隔线以后,第一行到第七行的结果。查阅ArrayList集合contains方法的API文档描述为:当且仅当此列表包含至少一个满足(o==null ? e==null : o.equals(e))的元素e时,则返回true,其中o是contains方法传入参数对象,e为equals方法传入参数象。

       首先,从list集合中取出Person("Jack", 23),o==null为假,执行冒号以后的语句,因为此时temp集合中不包含任何元素,e指向null,因此contains方法最终返回假,if语句中非假为真,将Person("Jack",23)存入temp集合中,此时并没有打印任何Person对象的信息。

       此后,再从list集合中取出Person("Peter", 31),将"Peter"对象(这里我们以对象的name属性来称呼对象)传入contains方法后,开始遍历temp集合,调用"Peter"的equals方法,依次将temp集合中的元素与"Peter"比较,此时由于只有"Jack"对象,因此在打印了"Peter"的姓名以后,仅打印了"Jack"的姓名就结束了遍历。遍历完毕后并没有找与"Peter"相同的对象,接着就将"Peter"存入了temp中。

       第三步,取出了"Lucy"对象,重复第二步的动作,在打印"Lucy"姓名的同时,先后打印了"Jack"和"Peter"的姓名,并将"Lucy"存入temp中。

       第四步,又从list中取出了"Jack"对象,开始遍历temp集合中的元素,依次传入"Jack"对象调用的equals方法中进行判断。遍历第一个元素"Jack"后就检测到了相同元素,因此仅打印了"Jack"的姓名,之后并没有继续遍历。

       最后,又从list集合中取出了"Lucy"对象,遍历temp集合,同时通过"Lucy"对象的equals方法进行判断,分别打印了"Jack"和"Peter"的姓名以后,检测到相同元素"Lucy",打印了"Lucy"的信息后,并没有将其存入。这就是分隔线以后打印元素信息的过程。

       那么通过contains方法的原理演示,我们也可以将该原理推广到ArrayList的其他方法:比如remove方法,将需要删除的元素传入remove方法后,remove方法底层就会去遍历集合中的元素,遍历的同时调用参数对象的equals方法,判断集合中的元素是否与该对象相同,如果相同就删除;否则就不进行任何动作。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值