数组:
数组是指将相同类型的元素按照一定顺序进行排列的集合,数组的存储是一块固定长度的连续的存储空间。
eg:当我们在声明一个数组时是需要指定其长度的
int[] data = new int[10];
数组的存储是连续的,因此只要知道第一个元素的存储位置,对应的就能知道后面所有元素的位置,
随机访问一个元素的时间复杂度是O(1)。
数组删除/增加一个元素,需要移动删除/增加位置之后的所有元素,时间复杂度是O(n)。
链表:
链表是一种非连续、非顺序的结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,链表由一系列结点组成。
链表的存储位置是无法确定的,通过一个节点的指针指向才能确定另一个节点的存储位置,因此随机访问一个元素的
效率是比较低的,需要遍历才能确定各个节点的存储位置,时间复杂度是O(n),但是相比较与数组,链表的删除/插入
的效率是比较高的,它不需要移动链表内的元素,只需要改变删除/插入左右两边元素的指针的指向,时间复杂度是O(1)。
常见的链表有:单向链表/双向链表/循环链表
//eg:java实现的类似链表结构的节点类
class Node<E> {
E element; //当前节点元素
Node previous; //上一个节点
Node next; //下一个节点
public Node(E element, Node previous, Node next) {
this.element = element;
this.previous = previous;
this.next = next;
}
}
栈:
数据遵循后进先出原则(LIFO)
简单分析下Java中Stack类。
/*
* Stack继承自Vector类,因此也是基于数组的操作,
* 如何实现后进先出的呢?
* 很简单,Stack数据的操作主要分为入栈和出栈,当控制数据的进入和弹出都只能从一个方向操作时,即实现了后进先出
* eg:依次放入abcd:a<-----b<------c<-------d(从右边依次放入)
* 此时若从左取出则不符合后进先出,只能从同一个方向取出操作(即右边),则取出顺序为(dcba)。
* 下面看下java中Stack的入栈和出栈是不是这么一回事?
*/
//入栈操作
public E push(E item) {
addElement(item);//执行了添加一个元素的方法
return item;
}
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);//数组扩容方法,这里不展开描述,在ArrayList再做说明
elementData[elementCount++] = obj;//扩容后将数据添加到当前数组的后面
}
//出栈操作
public synchronized E pop() {
E obj;
int len = size();
obj = peek();//获取到数组最末尾的元素的引用
removeElementAt(len - 1);//将数组最末尾的元素清除
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
队列
数据遵循先进先出原则(FIFO)
数据从一个方向进入,另外一个方向移除。通过上边栈操作稍微改动就能实现。这里不再展开分析
Collection集合通用接口定义
Collections集合常见操作的工具类
List:
存放有序(和添加的顺序一致),可重复的元素。
ArrayList:
底层是通过数组来实现的,但是不需要考虑数组长度越界,当元素超出固定长度后,会进行容量的扩充。非线程安全
//eg:在增加元素时,数组容量进行了扩张
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);
}
LinkeList:
底层是通过链表的实现,非线程安全
Set:
存放不可重复的元素,放入Set的类必须实现equals和hashCode方法(比较两个对象是否是同一个对象的方法)
HashSet:无序,不可重复对象。不保证元素的添加顺序,底层采用 哈希表算法,查询效率高。判断两个元素是否相等,equals() 方法返回 true,hashCode() 值相等。
即要求存入 HashSet 中的元素要覆盖 equals() 方法和 hashCode()方法
TreeSet:有序,不可重复对象,添加自定义对象的时候,必须要实现 Comparable 接口,并要覆盖 compareTo(Object obj) 方法来自定义比较规则
Map:
Map存储的是两个集合之间映射关系,以key/value的形式形成映射可看成一条数据,key不允许重复,value可重复
HashMap:采用哈希算法,key不可重复,所以key对象需重写equals和hashCode方法。
HashMap的原理分析:采用数组+链表的方式实现(散列表)。
利用数组存放哈希表的关键字key,通过哈希函数就能够快速得到其存储的位置,但是为了处理hashcode冲突,即对某个元素
通过哈希函数获得存储地址,发现该位置已经被占用。
解决办法,采用链表,当发现某个位置被占用时,看是否是同一个元素,如果是,直接将元素存入该位置,如果不是,则将该元素散列(创建新的地址存放),然后关联到链表的next。
散列表:散列表用数组加链表实现。数组的每一列称之为桶,根据数据的hashcode与桶的长度取余,得到的余数就是该元素所在桶的索引,如果当前桶的位置还没有其他的元素,则直接将该元素插入到该位置,如果该位置已经存在了其他元素(散列冲突),比较两个元素是否一致,如果不一致则将该元素添加到该桶的链表的头部。
TreeMap:同理,红黑树实现有序,key必须要实现 Comparable 接口,并要覆盖 compareTo(Object obj) 方法来自定义比较规则。
HashMap是非线程安全的,
HashTable是线程安全的,通过查看源码的实现可以看到,对其的操作方法上都加上了同步锁(Synchronized),不推荐使用,使用多线程目的是实现多个任务的并行执行,添加同步锁之后,数据的访问因为持有了锁,需要锁的获取和释放,线程对数据的访问是串行的,并且还增加了锁的获取释放的消耗。
Java中的免锁集合:
CopyOnWriteArrayList、CopyOnWriteArraySet、ConCurrentHashMap。
- CopyOnWriteArrayList:
采用读写分离的思想 ,当对List进行修改操作的时候,先复制出一份,然后修改复制出来的List,这时候如果有其他线程读取List的话读取到的是旧的数据,当修改操作完成后会将该对象的引用指向修改完后的数据,这时候读取数据将读取到新指向的数据,当然,修改操作必须是加锁的,否则会copy多个副本出来。下面我们通过源码来证实上面的说法是否正确:
public boolean add(E e) {
synchronized (lock) {
//可以看到此处加了同步锁
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//复制新的Array,同时容量扩充了1
newElements[len] = e;//复制出的Array执行了增加的操作
setArray(newElements);//将对象的引用指向新的Array
return true;
}
}
知道了原理之后,可以可自己手动试试写一个CopyOnWriteMap.
存在的问题:
- 内存占用,增加了内存负荷,频繁GC,导致相应响应的时间变长。
- 数据一致性:该List只能保证数据最终的一致性,如果需要数据修改后立马能够读取到修改后的数据不建议使用。
2.ConCurrentHashMap:
分段加锁,减小并发冲突。