常用的单列集合对象(Collection)实现原理详解

1、概念

集合:存储对象的容器。java面向对象的语言,对事物都已对象的形式来描述,所以为了对多个对象进行操作存储,集合是存储对象常用的方法;

2、集合与数组的区别

相同点
1、集合与数组都是容器;
异同点
1、数组的长度是固定,而集合长度可变;
2、数组可以存储基本数据类型,集合只能存储对象数据;
3、数组存储数据类型是单一的,集合可以存储任意类型的对象;

3、集合的继承关系(分类):

————| Collection:是所有单列集合的父类
——————| List : 有序,可重复的(按插入顺序排序);
————————| ArrayList:内部维护了object[ ] 数组,查询块,增删慢;
————————| LinkedList:链表数据结构,增删块,查询慢,线性不安全;
————————| Vector:与ArrayList原理安全,但是同步的,线程安全,效率较低
——————| Set : 无序,不可重复;
————————| HashSet:存取速度快,线程不安全,底层是哈希表实现;
————————| TreeSet:红-黑树的数据结构,对具有自然顺序的数据进行自然排序,

4、ArrayList解析

4.1、ArrayList原理

我们先看一下源码:
这是ArrayList()无参的构造方法:

 /**
 /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access
    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

上面的代码我们可以看到,声明一个无参的ArrayList()对象,其实只是声明了一个空的object数组,并且没有指定长度,那么长度是在什么时候指定的尼?我们来看他的add()方法:

    /**
     * Default initial capacity.
     */
 private static final int DEFAULT_CAPACITY = 10;
 
 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

从上面的那一串的源码我们可以解析出,声明的ArrayList()对象是在调用add()方法时才进行长度声明的。并且在每次调用add()方法时,都会进行一连串的数组长度判断,如果长度不够时,进行动态扩容,扩容后的大小:int newCapacity = oldCapacity + (oldCapacity >> 1);也就是原来的0.5倍。实际上的扩容都是生成一个长度为newCapacity的新数组,再调用Arrays.copyOf()方法,将原来数组中的内容复制到新数组中去(复制数组这一过程是比较耗时的),之后再添加新元素;以上也正是为什么ArrayList()新增慢的原因了;

那为什么ArrayList对象的查询是比较快呢?
ArrayList查询块是与数组的特性有关系的,
那说一下数组的特性:数组里元素与元素之间的内存地址是连续的;
也就是说,在ArrayList里,我要查询第100的那一个元素,那么直接将第一个元素的内存地址加上100就可以得到第一百的那一个元素了;

为什么ArrayList对象的删除比较慢呢?

 public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

看源码,在remove()方法执行里都有复制数组这一操作在里面,而这一操作是比较耗时的;之所以进行这一操作是因为,举个例子:假如删除ArrayList里面居中的某个元素,那么数组中间就会有一个空格,这是会将这一元素的右边的其他元素向左移动,补全移除后的空格,这一动作也就是复制数组了,所以耗时长。

4.2、ArrayList总结:

ArrayList的数据结构是一个Object[ ],使用无参的构造方法声明一个ArrayList()的时候,默认分配的长度是10,当长度不够时,可以动态扩容,扩容量为原来的0.5倍;
ArrayList的增删较慢是因为这一过程需要进行一系列的数组长度判断以及复制数组的操作在里面;
ArrayList的查询块是因为数组的特性:数组里元素与元素之间的内存地址是连续的,所以查询第某个元素,直接第一个元素的内存地址加上第多少个的排序就可以了;

5、LinkedList解析

5.1、基本概念

LinkedList:是双链表的数据结构,将一个单元分成两个部分,一个部分存储元素,一部分存放下一个元素的内存地址。具有增删块,查询慢的特点;
查询:LinkedList里元素的内存地址并不连续,需要上一个元素记住下一个元素的地址,查询的时候需要从头往下找(迭代),明显没有数组查询块。
新增:链表在插入元素时,直接让上一个元素记住新元素的地址,让新元素记住下一个元素的地址,这样插入就快。
删除:删除时让前一个元素记住后一个元素, 后一个元素记住前一个元素. 这样的删效率较高。

5.2、图解LinkedList增删数据

5.2.1、一般的顺序添加:
在这里插入图片描述
根据添加顺序,在狗娃里存放狗蛋的内存地址,狗蛋里存放铁蛋的内存地址,以此类推,,,
5.2.2、删除元素的图解
在这里插入图片描述
5.2.3、两元素之间插入数据
在这里插入图片描述

5.3、LinkedList总结

LinkedList是采用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快,哦,LinkedList是线性不安全的;

6、Vector解析(了解即可)

6.1、基本概念

Vector :底层与ArrayList一样,也是维护了object[ ] 数组,实现方法也和ArrayList一样,但是Vector是线性安全的,效率较低;

6.2、Vector和ArrayList的异同

相同点
内部都是维护了object[ ] 数组;
异同点
1、Vector是多线性安全的,效率较低;
2、ArrayList是单线程不同步的,效率较高;
3、Vector是JDK1.0出现的,而ArrayList是JDK1.2出现的;

6.3、总结

Vector描述的是一个线程安全的ArrayList。

7、HashSet解析

7.1、基本概念

HashSet底层是哈希表来实现的,元素不可重复,线程不同步,但是存取速度快;

7.2、运作原理

为什么HashSet存取速度快?
哈希表存放的是哈希值,HashSet存储元素的顺序不是按照插入时的顺序来存储的,而是根据哈希值来存储,获取元素也是按照哈希值来进行获取的。正是因为按照哈希值来存取元素,所以速度快;
HashSet是怎么判断元素为重复元素的?
首先介绍一下哈希表的特点:桶式结构,就是哈希表的一个存储位置可以存储多个元素;
HashSet存放元素时,会先调用元素的hashCode()方法,获取到元素的哈希值(默认为内存地址),再经过哈希特有移位等运算就可以得到元素在哈希表中的存储位置了;
得到元素在哈希表中的存储位置之后,还发两种情况
1、如果该存储位置没有其他的元素,则直接将该元素存放在该位置上;
2、如果该存储位置有其他的元素,这是调用元素的equals()方法,判断两个元素是否相同。如果equals()方法返回true,则视为该元素与这个位置上的其他元素重复,不进行存储。如果equals()方法返回false,则直接在这个位置上存放该元素;
所以用HashSet存储自定义数据类型的时候,得重写对象的hashCode()equals()方法

下面举一个例子:
自定义一个Book类,并且重写了它的hashCode()equals()(使用id来判断是否相同)方法,如下:

public class Book implements Comparable<Book>{
 	String name;
	int id;
	int price;
	
    public Book(String name, int id) {
		this.name = name;
		this.id = id;
	}
    @Override
	public int hashCode() {
		return super.hashCode();
	}
	@Override
	public boolean equals(Object obj) {
		Book book = (Book) obj;
		return this.getId() == book.getId();
	}
}

然后再上一张图,来模拟存储的过程:
在这里插入范德萨描述z

7.3、注意要点

1、HashSet判断元素是否相同,先调用的是hashCode()方法,再者才有可能调用equals()方法(hashCode方法返回值相同,调用equals,否则不调用);
2、HashSet与ArrayList都有boolean contains(Object o)方法,但是HashSet使用hashCode和equals方法,ArrayList使用了equals方法。

8、TreeSet解析

8.1、基本概念

TreeSet采用的是红-黑树的数据结构,使用元素的自然顺序对元素进行排序,自然顺序比如:‘a’,“a”,1,…
当然,如果元素不具备自然顺序,那么对元素进行排序就有两个方法:
方法一:让该元素所属的类得实现Comparable接口,重写compareTo方法,指定比较规则,根据 compareTo方法的返回结果来进行判断大小;
方法二:在创建TreeSet对象的时候在构造方法里传入一个比较器,根据 比较器返回结果来进行判断大小,比较器的规则如下:
自定义一个类实现Comparator接口即可,把元素与元素之间的比较规则定义在compare方法内即可。
自定义比较器的格式 :

class  类名  implements Comparator{
 	   @Override
	   public int compare(Object o1, Object o2) {
	    。。。
	   }
 }

无论是方式一,还是方式二,方法的返回值都是有以下几种结果:
1、返回值为负整数、零或正整数,根据此来分别判断是小于、等于还是大于指定对象;
2、如果返回为0,(等于),则判断次元素为重复元素,不得添加。

8.2、例子

8.2.1、方式一例子

创建一个Book类,实现了Comparable接口,重写了compareTo方法,并且在compareTo里打印出了比较过程:

public class Book implements Comparable<Book>{
	private String name;
	private int id;
	private int price;
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public int getPrice() {
		return price;
	}
	public void setPrice(int price) {
		this.price = price;
	}
	@Override
	public String toString() {
		return "Book {编号:" + id +",书名:" + name + ",价格:"+price+ "}";
	}
	public Book(String name, int id) {
		this.name = name;
		this.id = id;
	}
	public Book(int id,String name,int price) {
		this.name = name;
		this.id = id;
		this.price = price;
	}
	@Override
	public boolean equals(Object obj) {
		Book book = (Book) obj;
		return this.getId() == book.getId();
	}
	@Override
	public int hashCode() {
		return super.hashCode();
	}
	@Override
	public int compareTo(Book bk) {
		System.out.println(this.name+"--->"+bk.name);
		return this.getId()-bk.getId();
	}
}

接下来是主方法:

public class TreeSetDemo {
	public static void main(String[] args) {
		Set<Book> tree = new TreeSet<Book>();
		tree.add(new Book(120,"敏捷开发",500));
		tree.add(new Book(110,"极限编程",550));
		tree.add(new Book(123,"消息队列",360));
		tree.add(new Book(155,"编程思想",555));
		tree.add(new Book(985,"数据结构",752));
	}
}

来看一下重写的compareTo()方法,可以得到是使用Bookid来排序的:

@Override
	public int compareTo(Book bk) {
		System.out.println(this.name+"--->"+bk.name);
		return this.getId()-bk.getId();
	}

主线程的运行结果看下图:
在这里插入图片描述
运行的大致过程如下:
1、首先添加敏捷开发,自己与自己比较之后,作为根节点;
2、再添加极限编程极限编程id=110小于根节点敏捷开发的id=120,根据红-黑树的规则:左小右大,所以极限编程放在敏捷开发的左边;
3、接下来添加消息队列消息队列id=123大于根节点敏捷开发的id=120,根据红-黑树的规则:左小右大,所以消息队列放在敏捷开发的右边;
4、紧接着添加编程思想编程思想id=156大于根节点敏捷开发的id=120,根据红-黑树的规则:左小右大,所以消息队列放在敏捷开发的右边。之后编程思想,id=156再与消息队列,id=123进行比较,由于编程思想大于消息队列,所以编程思想放在消息队列的右边;
5、之后添加数据结构,id=985数据结构,id=985大于根节点敏捷开发,id=120,放在敏捷开发的右边,再与消息队列比较,大于消息队列,放在消息队列的右边。然后再跟编程思想比较,并大于编程思想,放在编程思想的右边;
注意:TreeSet每添加一个元素,都是从根节点开始比较的,另外红-黑树是能自动的调节根节点的;

8.2.2、方式二例子

创建一个Book类,不实现了Comparable接口。再自定义一个MyCompare类并实现Comparator,重写compare方法:

class MyCompare implements Comparator<Book>{

	@Override
	public int compare(Book o1, Book o2) {
		if (o1.getPrice() == o2.getPrice()) {
			return o1.getId() - o2.getId();
		}
		return o1.getPrice() - o2.getPrice();
	}
}

在这一个compare方法里可以看到,主要是使用price来进行排序,如果有价格相等的,再使用id进行排序
接下来再看主线程

public class TreeSetDemo {
	public static void main(String[] args) {
		MyCompare compare = new MyCompare();
		Set<Book> tree = new TreeSet<Book>(compare);
		tree.add(new Book(120,"敏捷开发",500));
		tree.add(new Book(110,"极限编程",550));
		tree.add(new Book(123,"消息队列",360));
		tree.add(new Book(155,"编程思想",555));
		tree.add(new Book(985,"数据结构",752));
	}
}

在主线程里将MyCompare对象作为实参放在TreeSet的构造器里;
它的运行过程与方式一差不多,所以此处略过

8.3、注意要点

1、红-黑树(二叉树)的规则是左小右大
2、TreeSet集合里添加元素时,如果元素本身不具备自然顺序的特性,而元素所属的类已经实现了Comparable接口, 在创建TreeSet对象的时候也传入了比较器那么是以比较器的比较规则优先使用。
3、在重写compareTo或者compare方法时,必须要明确比较的主要条件相等时要比较次要条件。(如果只是比较书的价格,当两本书的价格相同时,之后添加的那一本将被视为重复,这不符合规律。所以还要准备次要的条件,比如书的编号,当价格相同时,再比较编号)
4、一般是推荐使用比较器(Comparator)的,因为复用性较强。
5、TreeSet与hashCode()equals()方法是没有任何关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值