List集合的特性及源码解析

版权声明: https://blog.csdn.net/catninth/article/details/80356690

这段时间整理了集合的相关内容,另外之前也承诺将更新hashmap内容,一并做个总结,尽量从底层实现的基础上去解释相关类的特性,由于篇幅限制将分为两篇讲解

集合collection一般分为常用的两个接口list和set

list:可重复,有序。

set:不可重复,无序。
list 常用的两个实现类,ArrayList、LinkedList,他们都是线程不安全的。
ArrayList:基于数组,容量为10,每次扩容增加一半,查询快,增删慢,下面结合源码实现思路来讲解
ArrayList在设计了一个size指针,用来记录当前数据存储量,当size超过数组长度时,通过System.arrayCopy方法来复制扩容,而且每次增删数据都用调用此方法来处理,被增删元素之后的所有元素,也就是说增删操作都要复制新建数组对象,所以效率较低,我写了山寨(大体实现了可变数组)的底层如下:
class ArrList {
private String[] data;
// 记录元素的个数
private int size = 0;
public ArrList() {
data = new String[10];
}
public ArrList(int initialCapacity) {
// 判断容量是否合法
if (initialCapacity < 0)
throw new IllegalArgumentException("参数不合法!!!");
// 初始化数组
data = new String[initialCapacity];
}
private void grow() {
if (data.length <= 1)
data = Arrays.copyOf(data, data.length + 1);
else
data = Arrays.copyOf(data, data.length + (data.length >> 1));
}
public void add(String str) {
// 判断数组是否需要扩容
if (this.size >= data.length) {
this.grow();
}
data[size++] = str;
}
public void add(int index, String str) {
// 判断下标越界
if (index > size || index < 0) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
// 判断扩容
if (size >= data.length)
this.grow();
System.arraycopy(data, index, data, index + 1, size - index);
data[index] = str;
size++;
}
public int indexOf(String str) {
for (int i = 0; i < size; i++) {
if (data[i] == str || data[i] != null && data[i].equals(str)) {
return i;
}
}
return -1;
}
private void out(int index) {
if (index >= size || index < 0) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
}
public void remove(int index) {
// 判断下标越界
this.out(index);
// 移除元素
System.arraycopy(data, index + 1, data, index, size - (index + 1));
size--;
}
public void remove(String str) {
// 先找到这个元素的位置
int index = this.indexOf(str);
if (index != -1) {
this.remove(index);
}
}
public void clear() {
// 这里直接设定指针,不对数据做处理较为高效

size = 0;}

最后注意System.arraycopy是调用的native本地方法,即用java申明但是调用操作系统C语言实现的,所以相对于java复制数组而言相对相率较高,但是对比链表的增删效率仍然较低。

LinkedList:本质上是个双向链表,通过Node节点记录前后节点的地址和本身数据地址,也就是说链表的元素内存并非是连续,而是散列的,增删数据实际上,只是对被处理元素的前后Node的节点地址重新设定,效率较高。中间元素处理时还是需要迭代next方法到目标位置,然后进行处理。其内部设定了size,firstnode,lastnode三个变量记录开始和末尾节点以及链表大小,当处理两端数据时,不需要迭代只用将端节点修改地址即可,效率非常高!我自己写了山寨版的源码用来锻炼对此的理解:

public class NoteLinkedList<T> {
int size = 0;
Note startnote=null;
Note endnote=null;
public void add(T t){
this.add(size,t);

}

//重点理解第一个add方法

public void add(int index, T t){
if(index>size || index<0)
throw new IndexOutOfBoundsException("越界了");
Note note = new Note();
note.setObj(t);
if(size==0){
this.startnote = note;
this.endnote = note;
}else if(index==0){
note.setNextnote(this.startnote);
this.startnote.setPrenote(note);
this.startnote = note;
}else if(index==size){
note.setPrenote(this.endnote);
this.endnote.setNextnote(note);
this.endnote=note;
}else{//下面可以看到链表非两端数据时,需要迭代多次,还是比较麻烦的。
Note flag = this.startnote;
for(int i=0; i<index; i++){
flag = flag.getNextnote();
}
flag.getPrenote().setNextnote(note);
note.setPrenote(flag.getPrenote());
flag.setPrenote(note);
note.setNextnote(flag);
}
this.size++;
}
public void set(int index, T t){
if(index>=size || index<0)
throw new IndexOutOfBoundsException("越界了");
Note flag = this.startnote;

for(int i=0; i<index; i++){
flag = flag.getNextnote();
}
flag.setObj(t);
}
public boolean isEmpty(){
return this.size==0;
}
public int size(){
return this.size;
}
public boolean contains(T t){
if(this.size==0)
return false;
Note flag = this.startnote;
for(int i=0; i<this.size; i++){
if(flag.getObj().equals(t))
return true;
flag = flag.getNextnote();
}
return false;
}


@SuppressWarnings("unchecked")
public T get(int index){
if(index>=size || index<0)
throw new IndexOutOfBoundsException("越界了");
Note flag = this.startnote;
for(int i=0; i<index; i++){
flag = flag.getNextnote();
}
return (T)flag.getObj();
}

public void remove(int index){
if(index>=size || index<0)
throw new IndexOutOfBoundsException("越界了");
if(index==0){
this.startnote=this.startnote.getNextnote();
this.startnote.setPrenote(null);
}else if(index==size){
this.endnote=this.endnote.getPrenote();
this.endnote.setNextnote(null);
}else{
Note flag = this.startnote;
for(int i=0; i<index; i++){
flag = flag.getNextnote();
}
flag.getPrenote().setNextnote(flag.getNextnote());
flag.getNextnote().setPrenote(flag.getPrenote());
}
this.size--;
}

public void clear(){
for(Note flag=this.startnote; flag!=null;){
Note next = flag.getNextnote();
flag.setPrenote(null);
flag.setObj(null);
flag.setNextnote(null);
flag = next;
}
this.size=0;
}

}


class Note{
private Note prenote;
private Note nextnote ;
private Object obj;
public Note getPrenote() {
return prenote;
}
public void setPrenote(Note prenote) {
this.prenote = prenote;
}
public Note getNextnote() {
return nextnote;
}
public void setNextnote(Note nextnote) {
this.nextnote = nextnote;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}

}

其实能将第一个add方法理解透,对整个链表就可以有比较好的认知了。

另外还有一个SkipList增删查询效率都比以上两种表效率高,缺点由于要存储多层数据比较费内存,本质也是链表,但是对元素进行了排序,而且加了多层索引,比如第一层放三个元素(中间和首尾)将整体数据分为2部分,第二层放入五个元素将整体数据分为4部分,第三层放入9个元素分为8部分,依次类推一直到底层存放所有元素,这样可以通过逐层往下快速定位所需操作元素的位置。

另外还有个线程安全的表Vector(初始容量为10,扩容一倍),处理数据效率非常低,所有方法都用Sychronized关键字修饰锁住,一般我们在需要线程安全的时候才使用,比较典型的是SequenceInputStream合并流处理的时需要通过Enumeration的Element方法遍历不同的输入流,然后放入Vector中形成合并流。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页