Java集合
在Java中,有很多种方式可以用来表示集合,一般常用的就是Collection接口(编程时,一般使用的他的子类,List接口和Set接口)、Map接口和数组。
Collection
List
-
ArrayList:底层数据结构是数组(动态数组),查询快(因为底层是数组,可以根据下标快速查找),增删慢,线程不安全,效率高,可以存储重复元素
-
什么是动态数组?我们知道,在Java中数组一旦初始化,那么长度就固定了,而动态数组不存在这个问题。
-
动态数组的实现原理:自己封装一个类用于实现,其实现动态数组的核心就是扩容方法,详情可查看另一位大神的文章(非本人创作)动态数组。
-
先申明一个类
public class Array<E>
-
成员变量
private int size; //数组中元素的个数 private E[] data; //数组声明
-
构造方法
// 有参构造 public Array (int capacity) { data = (E[])new Object[capacity]; size = 0; } // 无参构造 public Array () { this(10); //调用有参构造方法,并默认初始容量为10 }
-
其他基本方法
//获得数组元素个数 public int getSize () { return size; } //获得数组长度 public int getCapacity () { return data.length; } //获得数组是否为空 public boolean isEmpty () { return size == 0; }
-
add方法:本质就是,从后往前到指定索引位置,每个元素向后移一个格,给新来的腾出个地方。因为需要一个一个向后移动,通过循环给后面的每一个元素重新赋值,所以插入方法比链表慢。
//向数组指定位置添加元素,index为指定索引位置,e为添加的值 public void add (int index, E e) { //索引位置不能让它瞎插,索引为负数,或者跳格子插,不可以。 if (index < 0 || index > size) { throw new IllegalArgumentException("add is fail, require index < 0 || index > size"); } //当数组容量满了的时候,调用扩容方法,此处给它扩当前数组长度的两倍。 if (data.length == size) { this.resize(data.length * 2); }for (int i = size - 1; i >= index; i--) { data[i+1] = data[i]; } //新来的进坑 data[index] = e; //维护size size ++; }
-
删除方法(一种根据索引删除,一种根据值删除):和添加相反,从要删除的索引位置的下一位开始,到最后一位元素索引位置结束,依次向前占一个,方向是从前向后,与add相反。
//根据索引删除某个元素 返回删除的元素 public E remove (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("remove is fail,require index < 0 || index >= size"); } //先把要删除的元素存起来,不然等会就给覆盖了。 E value = data[index]; for (int i = index + 1; i < size; i++) { data[i-1] = data[i]; } //维护size size --; //此处为什么设置为null呢,因为泛型的原因,传进来的都是类对象,数组中存的是引用地址,引用不断开的话,垃圾回收器没办法回收。 data[size] = null; //此处缩容,当数组元素个数等于数组长度四分之一时,进行缩容if (size == data.length/4 && data.length / 2 != 0) { //缩容为数组长度的二分之一 this.resize(data.length /2); } return value; } //根据值删除某个元素 public void removeByValue (E e) { //复用根据值查找元素的方法,返回索引(此方法在下面) int index = this.getByElement(e); if (index != -1) { //复用根据索引删除的方法 this.remove(index); } }
-
查找方法**(分为两种,一种根据索引,一种根据值**)
//根据索引查找数组某个元素,返回值 public E getByIndex (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("get is fail, require index < 0 || index >= size"); } return data[index];// 根据索引查找速度就非常的快了 } //根据值查找数组某个元素,返回索引 public int getByElement (E e) { //本质:遍历数组进行比对 for (int i = 0; i < size; i++) { if (data[i].equals(e) ) { return i; } } return -1; } //是否包含该元素 public boolean contains (E e) { //本质:遍历数组进行比对 for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return true; } } return false; }
-
修改方法
//修改数组某个元素 public void set (int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException("set is fail, require index < 0 || index >= size"); } data[index] = e; }
-
扩容方法:这个个人认为就是动态数组的核心,当动态数组的容量达到最大后,调用扩容方法,将一个长度更大的数组初始化,重新引用给原本的数组,同时将原本存储的数据也都给赋值过去。
private void resize (int newCatacity) { E[] newData = (E[])new Object[newCatacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } //给成员变量data重新赋值新引用(后面有内存图介绍) data = newData; }
-
-
-
LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
-
链表不像数组那样存储了下标,链表只存了next,item,prev,所以查询时比较麻烦,因为没有下标,但是插入就很方便。当我们有新元素插入时,只需要修改所要插入位置的前一个元素的next值和后一个元素的prev值即可,如下图:
-
链表的底层是怎么实现的呢?
-
定义链表类
public class LinkedList<E>
-
定义成员变量
transient int size = 0;// 记录链表长度 transient Node<E> first;// 第一个链表元素 transient Node<E> last;// 最后一个链表元素
-
Node静态内部类:这个就是核心,一个静态内部类,定义在LinkedList类中,类中的属性就是next,item,prev
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
-
get方法:因为只存了next,item,prev3个属性,查询时只能通过循环一个一个的查找,所以链表查询较慢。
public E get(int index) { checkElementIndex(index);// 判断是否超出了链表的长度,超出了就报错 return node(index).item; } /** * 返回一个指定索引的非空节点. */ Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
-
add方法:插入时,只需要修改所要插入位置的前一个元素的next值和后一个元素的prev值即可,所以快。
public boolean add(E e) { linkLast(e); return true; } /** * 设置元素e为最后一个元素 */ void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } //在指定位置添加一个元素 public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } /** * 在一个非空节点前插入一个元素 */ void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
-
remove方法
//删除某个对象 public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } //删除某个位置的元素 public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } //删除某节点,并将该节点的上一个节点(如果有)和下一个节点(如果有)关联起来 E unlink(Node<E> x) { final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
-
-
Set
- HashSet:底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
- LinkedHashSet:底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
- TreeSet:底层数据结构采用二叉树来实现,元素唯一且已经排好序。
Map
HashMap
HashMap的数据结构为 数组+(链表或红黑树)
为什么采用这种结构来存储元素呢?
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。
那么存储的时候是怎么存储的呢?
- 第一步:计算出键的hashCode,该值用来定位要将这个元素存放到数组中的什么位置。
- 第二步:根据它的hashCode找到数组中的位置,判断是否为空,如果这个位置为空,就直接扔进去,如果不为空,那么判断两者是否相等,相等则直接***覆盖***,如果不等则在原元素下面使用链表的结构存储该元素。
- 第三步:因为链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到**8的时候使用链表存储就转变成了使用红黑树存储,原因就是红黑树是平衡二叉树,在查找性能方面比链表要高。
其节点存储,Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。所以这就是hashMap是数组+链表结构的原因。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
- put()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 判断数组是否为空,长度是否为0,是则进行扩容数组初始化
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过hash算法找到数组下标得到数组元素,为空则新建
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 找到数组元素,hash相等同时key相等,则直接覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 该数组元素在链表长度>8后形成红黑树结构的对象,p为树结构已存在的对象
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 该数组元素hash相等,key不等,同时链表长度<8.进行遍历寻找元素,有就覆盖无则新建
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 新建链表中数据元素,尾插法
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 链表长度>=8 结构转为 红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 新值覆盖旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent默认false
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 判断是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
-
get()方法
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 永远检查第一个node if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
TreeMap
TreeMap 是一个有序的key-value集合,继承于AbstractMap,它是通过红黑树实现的。
HashTable
Hashtable:线程安全的,不允许null的键或值。
数组
***数组的定义:***数组是相同类型数据的有序集合。数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们。
定义方式1:元素类型[ ] 数组名 = new 元素类型 [元素个数或数组长度]; // int [] arr = new int [3];
定义方式2:元素类型[ ] 数组名 = new 元素类型 []{元素1,元素2 …}; // int [] arr = new int []{1,2,3};
定义方式3:元素类型[ ] 数组名 = {元素1,元素2 …}; // int [] arr = {1,2,3};
在Java中,数组也是属于集合,只是它和其他的集合还是有些区别。
- 数组可以储存基本数据类型和对象,而集合中只能储存对象(可以以包装类形式存储基本数据类型)。
- 数组的长度是固定的(初始化后长度就固定了),集合长度是可以改变的。
- 定义数组时必须指定数组元素类型,集合默认其中所有元素都是Object。
- 无法直接获取数组实际存储的元素个数,length用来获取数组的长度,但集合可以通过size()直接获取集合实际存储的元素个数。
- 集合有多种实现方式和不同的适用场合,而不像数组仅采用分配连续的空间方式。
- 集合以接口和类的形式存在,具有封装,继承和多态等类的特性,通过简单的方法和属性调用即可实现各种复杂的操作,大大提高软件的开发效率。
JDK提供的java.util.Arrays类,包含了常用的数组操作,Arrays类包含了:排序(Arrays.sort)、查找(Arrays.binarySearch,二分法查找 )、填充(Arrays.fill)、打印内容(Arrays.toString)等常见的操作。
扩展
数组转LIst效率最高的方法:
Integer[] intrArray = {1,2,3,4};
ArrayList< Integer> arrayList = new ArrayList<>(strArray.length);// 先定义好长度,可避免多消耗
Collections.addAll(arrayList, strArray);// 这样做和asList方法比起来效率更高
Object转LIst的方法:
public static List castList(Object obj, Class clazz) {
List result = new ArrayList();
if (obj instanceof List) {
for (Object o : (List) obj)
{
result.add(clazz.cast(o));
}
return result;
}
return null;
}
本文只详细总结了一些常用的,像ArrayList,LinkedList,HashMap,而有时候为了线程安全甚至都可能不用他们,而一些不常用的只是一笔带过。学无止境,奥利给就完事了。