Java开发笔记(六十七)清单:ArrayList和LinkedList

前面介绍了集合与映射两类容器,它们的共同特点是每个元素都是唯一的,并且采用二叉树方式的类型还自带有序性。然而这两个特点也存在弊端:其一,为啥内部元素必须是唯一的呢?像手机店卖出了两部Mate20,虽然这两部手机一模一样,但理应保存两条销售记录才是。其二,不管是哈希类型还是二叉类型,居然都不允许按照加入时间的先后排序,要知道现实生活中不乏各种先来后到的业务场景。为了更方便地应对真实场景中的各类需求,Java又设计了清单List这么一种容器,用来处理集合与映射所不支持的业务功能。
提到清单,脑海里顿时浮现出从上往下排列的一组表格,例如购物清单、愿望清单、待办事项等等,它们的共同点一是都有序号,二是按线性排列。清单里的元素允许重复加入,并且根据入伙的时间顺序先后罗列,这些特征决定了清单是种贴近日常生活的简易容器。不过Java中的List属于接口,实际开发用到的是它的一个实现类ArrayList(列表,又称动态数组)。在某种程度上,列表的确跟数组很像,比如二者的内部元素都分配了整数序号/下标、都支持通过序号/下标来访问指定位置的元素等等。但列表贵为容器中的一员,自然拥有几点数组所不能比拟的优势,包括但不限于:


一、列表允许动态添加新元素,不管调用多少次add方法,也不必担心列表空间不够用的问题。下面代码便演示了如何声明列表实例并对其依次添加元素:

		// 创建一个列表(动态数组),其元素为MobilePhone类型
		ArrayList<MobilePhone> list = new ArrayList<MobilePhone>();
		list.add(new MobilePhone("华为", 5000)); // 第一个添加的元素,默认分配序号为0
		list.add(new MobilePhone("小米", 2000)); // 第二个添加的元素,默认分配序号为1
		list.add(new MobilePhone("OPPO", 4000)); // 第三个添加的元素,默认分配序号为2
		list.add(new MobilePhone("vivo", 1000)); // 第四个添加的元素,默认分配序号为3
		list.add(new MobilePhone("vivo", 1000)); // 第五个添加的元素,默认分配序号为4

 

而数组的大小一经初始化设定就不可调整,除非另外给它分配新的数组空间;


二、数组只能对指定位置的元素进行修改操作,列表不但支持修改指定位置的元素(set方法),还支持在指定位置插入新元素(add方法),或者移除指定位置的元素(remove方法)。


三、数组只有两种遍历方式:按下标遍历、通过简化的for循环遍历。而列表支持多达四种的遍历方式,分别说明如下:
1、简化的for循环。该方式同样适用于数组和容器,具体的遍历代码示例如下:

		// 第一种遍历方式:简化的for循环同样适用于数组和容器
		for (MobilePhone for_item : list) {
			System.out.println(String.format("for_item:%s %d",
					for_item.getBrand(), for_item.getPrice()));
		}

 

2、迭代器遍历。该方式与利用迭代器遍历集合是一样,都要先获得当前容器的迭代器,然后依次调用迭代器的next逐个获取元素。利用迭代器遍历列表的代码如下所示:

		// 第一种遍历方式:简化的for循环同样适用于数组和容器
		for (MobilePhone for_item : list) {
			System.out.println(String.format("for_item:%s %d",
					for_item.getBrand(), for_item.getPrice()));
		}

 

3、索引遍历。这里的索引是以0开始的序号,对应于数组的下标,只不过列表通过get方法获取指定位置的元素,而数组通过方括号引用某个下标。下面是使用索引遍历列表的代码例子:

		// 第三种遍历方式:与数组通过下标访问相似,列表通过索引获取指定位置的元素
		for (int i = 0; i < list.size(); i++) {
			MobilePhone index_item = list.get(i);
			System.out.println(String.format("index_item:%s %d",
					index_item.getBrand(), index_item.getPrice()));
		}

 

4、forEach遍历。Java8之后,每种容器都支持联合应用forEach与Lambda表达式的遍历方式,该方式的遍历代码见下:

		// 第四种遍历方式:使用forEach方法夹带Lambda表达式进行遍历
		list.forEach(each_item -> System.out.println(String.format(
				"each_item:%s %d", each_item.getBrand(), each_item.getPrice())));

  

尽管列表对于大多数的业务场景来说够用了,可是仍旧无法满足部分特定的业务需求,因为ArrayList默认把新元素添加到列表末尾,也不存在默认的删除操作。而在计算机科学常见的数据结构当中,至少还有两种是列表所不能实现的,其中一个叫做队列Deque,另一个叫做栈Stack。
队列取材于生活中的排队场景,譬如春运期间大家在火车站排队买车票,虽然有个别人嚷嚷着“我要插队”且自顾自地插了进去,也有人忍受不了漫长的等待而中途放弃排队改为骑单车回家,但多数人都会循规蹈矩地从队尾开始排队,买了票之后从队首离队。于是排队业务就抽象成为这么一种队列结构:添加时默认往末尾添加,删除时默认从开头删除。
至于栈则取材于计算机系统的寄存器操作,栈的特点是里面保存的数据为先进后出(同时也是后进先出),即最早添加的元素会被最后移除、最晚添加的元素会被最先移除。基于栈具有的数据先进后出特性,它常用于保存中断时的断点、保存子程序调用后的返回点、保存CPU的现场数据、在程序间传递参数等等。就栈作为一种容器的角色而言,每次添加的元素会默认加到开头,且每次删除操作会默认删去开头的元素,从而实现后进先出/先进后出的机制。
然而不管是队列还是栈,它们的存储形式都如同清单那样线性排列,区别在于数据进出的默认方位。因此Java把队列、栈以及清单三者加以融合,推出了链表LinkedList(又称双端队列)这种数据结构,它一起实现了List与Deque接口,并在某种程度上模拟了栈的功能,从而变成专治各种不服的万能清单。
作为清单大家族的一员,链表LinkedList的基本用法与列表ArrayList相同,并基于它的三个祖宗分别进行了下列方法拓展:
1、在清单List的功能增强方面,补充了如下的扩展方法:
addFirst:添加到清单开头
addLast:添加到清单末尾
removeFirst:删除清单开头的元素
removeLast:删除清单末尾的元素
getFirst:获取清单开头的元素
getLast:获取清单末尾的元素
2、在队列Queue的功能实现方面,提供了如下的队列方法:
offer:添加到队列末尾
offerFirst:添加到队列开头
offerLast:添加到队列末尾
peek:获取队列开头的元素
peekFirst:获取队列开头的元素
peekLast:获取队列末尾的元素
poll:删除队列开头的元素
pollFirst:删除队列开头的元素
pollLast:删除队列末尾的元素
3、在栈Stack的功能模拟方面,添加了如下的额外方法:
pop:队列开头的元素出栈,相当于方法removeFirst和pollFirst
push:新元素入栈,相当于方法addFirst和offerFirst
总的来说,链表的数据存储兼顾清单和队列的组织结构,常用于对数据进出有特殊要求的场合,例如采取先进先出FIFO的队列操作,以及采取先进后出FILO的栈操作。



更多Java技术文章参见《Java开发笔记(序)章节目录

posted @ 2019-03-04 21:19  pinlantu  阅读(373)  评论(0编辑  收藏  举报