Java容器研究及优化篇(一)
在Java环境中,一切都是以对象角色而存在,我们需要一种机制来集中管理这些对象,并对其中每个对象进行方便的插入、检索、修改及删除操作,这种机制称之为集合,也叫做容器。而我们最为常用的容器包括:List、Set、Map、Stack及Queue,它们都有各自的特性和擅长,也有不同的继承接口,内部的数据存储算法或格式也有所不同。本篇为集合研究的第一部分,主要以满足使用为前提,对常用集合的使用进行介绍;而第二部分则为深入的研究下这些集合,并在最后给出个封装的工具供参考使用。
l 容器存放内容
l 容器编程方式
l 常用容器特性
l 堆栈队列实现
l 如何迭代容器
1、容器存放内容
在所有的java容器中,存放的内容都为对象类型,不论是基本数据类型、系统默认对象或是自定义的对象。如果类型为基本数据类型,那么容器会自动将其升级转为对应的封装对象类型,具体如下所示:
publicvoid test(String args) {
List<Object> lists = newArrayList<Object>();
// 基本数据类型
lists.add(1);
// 系统默认对象
lists.add(new Date());
lists.add(new String(args));
// 自定义的对象
Custom custom = newCustom("Jakey","13155578923");
lists.add(custom);
for(Object obj : lists) {
logger.log(Level.DEBUG,"数据类型:"+obj.getClass().getTypeName());
}
lists.clear();
lists = null;
}
显示结果:
2、容器编程方式
在java编程中,面向接口或是抽象编程是个不错的选择,而java中的容器编程也可以选择这种优雅的方式。一般,我们会将使用的具体的容器类向上转型为基础接口,只在创建时指定具体的子容器类,这样做的好处就是可以减少代码的修改,做到抽象和共用的效果,以List为例说明:
List<Object>lists = new ArrayList<Object>();
而List即为接口基类,ArrayList为List的派生子类,只不过增加了特有的功能实现。List的常用子类还有LinkedList,那么实现类似:
List<Object>lists = new LinkedList<Object>();
对于其它的容器类,如map、set、queue等方式与List类似,这里不做介绍。上面的面向接口方式,在Java容器中并不总是奏效,如果使用子类时,用到了子类新增的特有功能,而List接口并没有实现,那么自然而然就必须使用子类来具体实现,所以需要结合具体的使用而论。不过依经验而论,容器基础接口类的功能最为常用,因为基本可以满足业务需求哦。
注意:
面向接口或是抽象编程,是我们必须要掌握的技能,请查阅相关资料学习。
3、常用容器特性
在Java编程中,最为常用的容器有:List、Map、Set及Queue,他们各自有自己擅长的一面,可以从存储和性能两方面考虑他们的异同,下面具体从它们的性能和存储方式两方面介绍下:
A、List
List容器可以将内部元素维护在特定的序列中,默认是按照元素插入的顺序排列内部元素,其有两种类型的子类,分别为:
ArrayList:
该容器适合随机访问元素,查询获取元素,而不擅长在List中间插入和删除元素,所以适合用在存储和检索元素。
LinkedList:
该容器在随机访问元素较慢,适合耗用较低代价在List中插入和删除元素,同时,它的特性集相对ArrayList更丰富。
所以,在查询大于修改List存储结构的情况下,建议使用List容器;反之,极力推荐使用LinkedList容器,据实际使用需求而定。
B、Map
Map容器可以实现将对象映射到其它对象,实际上是通过对象的引用来实现,因为Map容器存放数据的格式是键-值形式,采用了高读写的离散算法来实现数据存取,所以它在速度上较快,而且其存储的值为对象的引用,元素的顺序是离散不确定的,通过该引用可以关联到对应的实体类对象,现有如下几种Map容器:
HashMap:
访问速度最快,键-值元素并没有排序,插入和删除速度相对较慢;
TreeMap:
访问速度较HashMap慢些,因为其保持了“键”处于排序状态;
LinkedHashMap:
访问速度较快,仅次于HashMap访问速度,同时其也保持了插入元素的次序,并通过散列提供了快速的访问能力。
HashTable:
HashTable是旧版的jdk中的容器,已经被新版的HashMap所替代,
而与HashMap的不同点如下:
HashTable是同步的,而HashMap非同步;
1)HashTable键-值都不允许为空,而HashMap反之都允许;
2)两者的遍历方式不同,前者使用Enumeration,后者使用Iterator;
3)哈希值使用不同,前者使用对象的hashCode,后者重新计算hash值;
4)默认大小不同,前者默认大小为11,并且以old*2+1增加,而后者默认大小为16,并以2的指数增长;
C、Set
Set容器不保存重复的元素内容,如果试图添加重复元素,则Set会阻止该行为,而常用的Set集合类型有:HashSet、TreeSet及LinkedHashSet,它们的存在足够解决实际元素存储的需求了。
HashSet:
出于性能考虑,HashSet内元素没有顺序可循,使用了散列算法。其维护的元素的顺序与TreeSet和LinkedHashSet都不同,因为它们存储元素的方式不同,所以HashSet的查询速度极快。
TreeSet:
与HashSet不同,TreeSet内元素是规律排序的,其元素存储在红-黑数据结构中,而HashSet使用散列函数生成。当然,其在查询方面,不如HashSet快些,适合查找小量排序元素及插入和删除频繁的情况使用。
LinkedHashSet:
该容器弥补了HashSet和TreeSet各自的不足,其内部元素即是排序,查询速度也比较快,速度介于HashSet和TreeSet之间,是因为内部使用链表形式排序存储元素,并且采用散列加快元素查询的速度。如果在不确定使用HashSet或是TreeSet时,就可以使用它了。
D、Queue
队列是先进先出的容器(FIFO),即从容器的一端插入元素,从另一端取出,并且取出元素的顺序与入容器的顺序是相同的,其在并发编程中使用比较频繁,是一种可靠的将对象从一个区域传递到另一个区域的方法。
在Java SE5中,引入了PriorityQueue队列,该队列内元素的顺序为将对象入队的自然顺序排序,也可以通过Comparator进行修改排序。PriorityQueue可以保证使用peek()、poll()及remove()时,获取到优先级最高的元素哦,前提我们只需要对入队的元素设置有限权重值即可。
E、Stack
“堆栈”通常是后进先出(LIFO)的容器,因为最后入栈的元素,最先弹出堆栈。鉴于Java设计中,Stack的设计不灵活,有缺陷,一般会使用LinkedList来替代实现更灵活的Stack类似功能,该实现将在下面第5部分介绍,这里不在介绍。
4、堆栈队列实现
这里我们使用LinkedList容器来灵活实现堆栈和队列功能,因为其已经具备直接实现栈和队列的功能方法,所以使用其实现比较顺理成章了。下面具体介绍下如何实现,这里会用到泛型的概念,这个会在后续文章介绍,这里各位只需要知道泛型是一个可以兼容不同对象类型的数据类型即可。
A、堆栈
Stack代码:
publicclass Stack<T> {
private Logger logger =null;
// 选用实现容器
private LinkedList<T> storage =new LinkedList<T>();
public Stack() {
logger = LogManager.getLogger(Stack.class.getName());
logger.debug("Java容器堆栈实现日志...");
}
// 元素压入堆栈
public void push(T v) {
logger.log(Level.DEBUG,"元素压入堆栈...,内容:"+v.toString());
storage.addFirst(v);
}
// 取出顶部元素
public T peek() {
logger.log(Level.DEBUG,"取出顶部元素...,内容:"+storage.getFirst().toString());
return storage.getFirst();
}
// 从栈删除元素
public T pop() {
logger.log(Level.DEBUG,"从栈删除元素...,内容:"+storage.removeFirst());
return storage.removeFirst();
}
// 判断栈是否空
public boolean empty() {
logger.log(Level.DEBUG,"判断栈是否空...,结果:"+storage.isEmpty());
return storage.isEmpty();
}
// 字符格式化栈
public String toString() {
logger.log(Level.DEBUG,"字符格式化栈...,结果:"+storage.toString());
return storage.toString();
}
}
单元测试代码:
@Test
publicvoid logPrintStack() {
Stack<Object> stack = newStack<Object>();
// 压入堆栈元素
stack.push("2016年");
stack.push("10月");
stack.push("19日");
// 获得堆栈元素
stack.peek();
// 堆栈的空判断
if(!stack.empty()) {
stack.pop();
}
// 字符串化堆栈
stack.toString();
}
删除元素前结果:
删除元素后结果:
请大家仔细对比下,删除元素前后的结果变化,会发现结果完全符合上面所论述的事实。需要注意的是,我们自定义的Stack与系统默认的Stack名字冲突,所以在使用时需要加上包名字路径(前提是不在同一个包)。
B、队列
Queue代码:
publicclass Queue<T> {
private Logger logger =null;
// 选用实现容器
private LinkedList<T> storage =new LinkedList<T>();
public Queue() {
logger = LogManager.getLogger(Queue.class.getName());
logger.debug("Java容器队列实现日志...");
}
// 元素压入队列
public void offer(T v) {
logger.log(Level.DEBUG,"元素压入队列...,内容:"+v.toString());
storage.addLast(v);
}
// 队列取出元素
public T peek() {
logger.log(Level.DEBUG,"队列取出元素...,内容:"+storage.getFirst().toString());
return storage.getFirst();
}
// 队列删除元素
public T poll() {
logger.log(Level.DEBUG,"队列删除元素...,内容:"+storage.getFirst());
return storage.removeFirst();
}
// 判断队列是否空
public boolean empty() {
logger.log(Level.DEBUG,"判断队列是否空...,结果:"+storage.isEmpty());
return storage.isEmpty();
}
// 字符格式化队列
public String toString() {
logger.log(Level.DEBUG,"字符格式化队列...,结果:"+storage.toString());
return storage.toString();
}
}
单元测试代码:
@Test
public void logPrintQueue() {
Queue<Object> queue = newQueue<Object>();
// 压入堆栈元素
queue.offer("2016年");
queue.offer("10月");
queue.offer("19日");
// 获得堆栈元素
queue.peek();
// 堆栈的空判断
if(!queue.empty()) {
queue.poll();
}
// 字符串化堆栈
queue.toString();
}
删除元素前结果:
删除元素后结果:
5、如何迭代容器
正如上面介绍了容器,容器中一般存在很多元素,我们不可能一一的手动获取,而是需要遍历容器的元素,并最终查看或是修改容器元素。那么,我们目前可以使用两种容器迭代器,分别为Iterator和Iterable。
A、Iterator
Iterator迭代器接口能做的事情有:
1)使用iterator()要求返回一个Iterator,其并返回第一个元素;
2)使用hasNext()检查容器序列中是否还有元素存在;
3)使用next()获得容器序列的下一个元素;
4)使用remove()删除新近返回的元素;
如果我们只需要向前遍历容器,并不需要修改容器本身,那么我们可以使用更遍洁的Iterable的foreach语法会更好,具体使用如下:
@Test
publicvoid logPrintScan() {
// Iterator
List<Object> lists = newArrayList<Object>();
lists.add("2016年");
lists.add("10月");
lists.add("19日");
Iterator<Object> it = lists.iterator();
while(it.hasNext()) {
String item = (String) it.next();
logger.log(Level.DEBUG,"容器元素如下:"+item);
}
lists.clear();
lists = null;
}
结果显示:
如果我们需要双向遍历容器集合,也需要修改元素容器本身结构,那么可以使用Iterator的子类型ListIterator,前提是其只能用来遍历List容器,不能遍历Map容器哦,具体用法如下:
@Test
public void logPrintScan() {
// Iterator
List<Object> lists = newArrayList<Object>();
lists.add("2016年");
lists.add("10月");
lists.add("19日");
// 前向遍历
ListIterator<Object> it2 = lists.listIterator();
while(it2.hasNext()) {
String item = (String) it2.next();
logger.log(Level.DEBUG,"容器前向遍历元素如下:"+item+"元素位置:"+it2.nextIndex());
}
// 后向遍历
while(it2.hasPrevious()) {
String item = (String) it2.previous();
logger.log(Level.DEBUG,"容器后向遍历元素如下:"+item+"元素位置:"+it2.previousIndex());
}
lists.clear();
lists = null;
}
结果显示:
B、Iterable
Iterable是Java SE5引入的新的接口,该接口包含了一个能够产生Iterator的iterator()方法,并且该Iterable接口被foreach用来在容器元素序列中移动。因此,如果创建了任何实现了Iterable的类,都可以将它用在foreach语法中。有些同学会问,为什么在容器中可以直接使用foreach,并不需要实现Iterable接口?答案是否定的,因为在Java容器中,Collections都已经实现了该接口(除了Map容器),具体实例如下:
@Test
publicvoid logPrintScan() {
List<Object> lists =new ArrayList<Object>();
lists.add("2016年");
lists.add("10月");
lists.add("19日");
// Iterable
for(Object item : lists) {
String itemStr = (String) item;
logger.log(Level.DEBUG,"容器遍历元素如下:"+itemStr);
}
lists.clear();
lists = null;
}
结果显示:
Java容器研究及优化(一)就介绍到这里,由于作者水平有限,如有问题请在评论发言或是QQ群(245389109(新))讨论,谢谢。
转载请标明出处,原创不易,请尊重作者劳作成果,标明转载地址:
http://blog.csdn.net/why_2012_gogo/article/details/52861232