常见数据结构
栈
栈:先进后出 存入称为入栈|压栈,取出称为出栈|弹栈,入口和出口在集合的同一侧
理解:子弹的弹夹,存弹,第一个存进去的子弹,是最后一个出来 ,也就是先进后出
队列
队列:先进先出 入口和出口在集合的两侧
理解:吃饭排队 队伍前面先打到饭,就先出去
数组
数组:查询快,增删慢
要把数组中索引是x的元素删除
必须创建一个新的数组,长度是源数组的长度-1
把源数组的其他元素复制到新数组中
再把新数组的地址赋值给变量arr
源数组会在内存中被销毁(垃圾回收)
数组的弊端:
1.数组初始化之后,长度不可变,不便于扩展
2.数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高,同时无法直接获取存储元素的个数
3.存储数据的特点单一
链表
链表:查询慢,增删快
链表中地址不是连续的,每次查询元素,都必须从头开始,也就是没有索引
链表结构,每次增加或删除元素,对链表的整体结构没有影响单向链表:链表中只有一条链子,不能保证元素的顺序(存储元素和取出元素的顺序有可能不一致)
双向链表:链表中有两条链子,有一条链子是专门记录元素的顺序,是一个有序的集合
树
计算机中的树
二叉树:分支不能超过两个,左边叫左孩子或左子树,右边叫右孩子或右子树
排序树||查找树 :在二叉树的继承上,元素是有大小顺序的
左子树比右子树小
平衡树:左子树的数量和右子树的数量相等
不平衡树:左子树的数量和右子树的数量不相等
红黑树:特点类似于平衡树,查询速度非常快,查询叶子节点最大次数和最小次数不能超过2倍
约束:1.节点可以是红色的或黑色的
2.根节点是黑色的
3.叶子节(空节点)点是黑色的
4.每个红色的子节点都是黑色的
5.任何一个节点到其每一个叶子节点的所有路径上黑色节点树相同
集合的分类
集合分为
Collection接口(单列集合,用来存储一个一个的对象)
List接口:元素有序,元素可重复
实现类: ArrayList\LinkedList\Vector
Set接口:元素无序,元素不可重复
实现类:HashSet LinkedHashSet TreeSet
Map接口:双列集合,保存具有映射关系 key-value
HashMap
其子类:LinkedHashMap
Hashtable
其子类:Properties : 用于读取配置文件
SortedMap
其子类:TreeMap
Collection接口的方法
Collection接口方法
* 添加元素:
* add(Object obj)
* add(Collection coll)
* 获取有效元素的个数
* size()
* 清空集合
* clear()
* 判断是否是空集合
* isEmpty()
* 判断是否包含某个元素
* contains(Object obj) : 是通过equals方法来判断是否是同一个
* contains(Collection c) :也是调用equals方法来比较,拿两个集合的元素挨个比较
* 删除
* boolean remove(Object obj) :通过equals方法判断是否是要删除的元素,只会删除找到的第一个元素
* boolean removeAll(Collection coll) 取当前集合的差集
* 取两个集合的交集
* boolean retainAll(Collection coll):把交集的结果存在当前集合中,不影响coll
* 集合是否相等
* boolean equals(Object obj)
* 集合转成对象数组
* Object[] toArray()
数组-->集合 Arrays.asList(arr)
* 获取集合对象的哈希值
* int hashCode()
* 遍历
* Iterator iterator() :返回迭代器对象,用于集合遍历
使用迭代器对象.next()方法,访问集合的元素
remove():删除集合中的元素
hasNext():判断是否有下一个元素
整体遍历
Iterator iterator = coll.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
使用循环判断迭代器是否有下一个元素,如果有就走循环,没有就不运行
也可以使用foreach遍历
for(数据类型 局部变量名:集合(数组)变量){
//局部变量就是值
}对于添加和删除操作,参数所在的类必须重写equals()方法
Collection子接口一:List
List接口
List接口
元素有序,且可重复,元素都有对应的索引
List接口的实现类:ArrayList LinkedList Vector
List常用方法
* add(int index,ele) :在index位置插入ele元素
* addAll(int index,Collection ele) 在index位置将ele中的所有元素添加进来
* get(int index) 获取指定index位置的元素
* indexOf(Object obj) 返回obj在集合中首次出现的位置
* lastIndexOf(Object obj) 返回obj在集合中最后出现的位置
* remove(int index) 删除指定位置的元素,并返回此元素
* set(int index,Object ele) 设置指定index位置的元素为ele
* List subList(int fromIndex,int toIndex) 返回从fromIndex到toIndex位置的子集合[fromIndex,toIndex)
实现类一:ArrayList
//创建对象
List list = new ArrayList()
线程不安全,效率较高
底层使用Object[] elementDate存储
ArrayList源码
jdk7情况下:
底层创建了长度为10的Object[] elementData,如果数组容量不够,则扩容,默认情况下扩容到原来长度的1.5倍,同时通过Arrays.copyOf()方法赋值到新数组中
如果确定要存多少元素,可以使用带参构造器 ArrayList ar = new ArrayList(20);
jdk8情况下
ArrayList list = new ArrayList()底层Object[] elementDate初始化为{},并没有创建长度为10的容量,第一次调用add()时,底层才创建了长度为10的数组,添加和扩容操作和jdk7一样
实现类二:LinkedList
对于频繁的插入或删除元素,使用LinkedList,效率高
新增方法:
* addFirst(Object obj) :将参数obj添加到集合中第一个位置
* addLast(Object obj) : 将参数obj添加到集合中最后的位置
* getFirst() : 获取集合中的第一个元素
* getLast() :获取集合中的最后一个元素
* removeFirst():删除集合中的第一个元素
* removeLast() 删除集合中的最后一个元素
底层使用双向链表存储
LinkedList源码
LinkedList list = new LinkedList();内部声明了Node类型的first和last属性,默认值为null
通过add()方法,将添加的值封装到Node中,创建了Node对象
其中,Node定义为:
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;
}
}
实现类三:Vector
多数操作与ArrayList相同,区别就是Vector是线程安全的,避免使用Vector
新增方法:
* 新增方法:
*addElement(object obj) 添加元素
*insertElementAt(Object obj,int index)向指定索引插入元素
*setElementAt(Object obj,int index)修改指定索引的元素为obj
*removeElement(Object obj) 删除指定元素
*removeAllElements() 删除所有元素
底层使用Object[] elementDate存储
Vector源码
jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组,在扩容方面,默认扩容为原来的数组长度的2倍
三个实现类的异同
*ArrayList和LinkeList线程都不安全,但执行效率要比线程安全的Vector高
* ArrayList内部实现基于动态数组的数据结构,LinkedList基于链表的数据结构,查找和设置,ArrayList要比其他两个快一点,
* 因为LinkedList要移动指针,Vector是同步类,属于强同步类,所以做任何操作都慢
* 对于增删改LinkedList效率块,因为ArrayList要移动数据
集合遍历
方式1:使用增强for循环遍历
for(类型 自定义名称 : 对象 ){}
方式2:使用迭代器
1.获取迭代器对象
Iterator 迭代器名称 = 集合对象.iterator();
2.判断是否有元素
while(迭代器名称.hasNext()){
//获取元素
迭代器名称.next()
}
Collection子接口二:Set
Set接口
Set接口没有提供额外的方法,且不允许包含相同的元素
Set判断两个对象是否相同不是使用==运算符,而是根据equals()方法
没有索引,不能使用普通for遍历
Set的实现类 HashSet、 LinkedHashSet、TreeSet
添加元素的过程,以HashSet为例
向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此时哈希值接着通过某种算法计算出HashSet底层数组中的存放位置(索引位置),判断数组此位置上是否已经有元素
如果此位置上没有其他元素,则元素a添加成功--->情况1
如果此位置上有其他元素b(或以链表形式存在的多个元素)则比较元素a与元素b的hash值
如果hash值不相同,则元素a添加成功 -->情况2
如果hash值相同,进而需要调用元素a所在类的equals()方法
equals()返回true,元素a添加失败
equals()返回false,则元素a添加成功-->情况3
对于添加成功的情况2和情况3来说,元素a 与已经存在指定索引位置上数据以链表的方式存储
jdk7:元素a放到数组中,指向原来的元素
jdk8:原来的元素在数组中,指向元素a
向Set中添加的数据,如果是自定义类的对象,那么自定义类需要重写equals方法和hashCode方法
其实现类一:HashSet
HashSet的特点:
* 不能保证元素的排列顺序
* 线程不是安全的
* 集合元素可以为null
* 底层是一个数组+链表(查询速度快)(jdk7) jdk8底层使用HashMap
* HashSet:判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的equals()方法返回值也相等
其实现类二:LinkedHashSet
LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。
优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet
LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的
LinkedHahSet插入性能略低于HashSet,在迭代访问set里所有元素的效率好
其实现类三:TreeSet
1.向TreeSet中添加的数据,要求是相同类的对象。
2.两种排序方式:自然排序(实现Comparable接口) 和 定制排序(Comparator)TreeSet默认用自然排序
底层使用红黑树结构存储数据,有序,查询速度比List快
* TreeSet新增的方法:
* Comparator compatator()
* Object first()
* Object last()
* Object lower(e)
* Object higher(e)
* SortedSet subSet(fromElement,toElement)
* SortedSet headSet(toElement)
* SortedSet tailSet(fromElement)
必须添加相同类的对象
自然排序
自然排序:TreeSet会调用集合元素的compareTo方法来比较元素之间的大小关系,然后将集合元素按升序排列
如果把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口
因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象
定制排序
TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法
仍然只能向TreeSet中添加类型相同的对象
Map
Map和Collection并列存在,用于保存具有映射关系的数据 key-value也叫kv对
* key用Set存放,不允许重复,(HashMap)key所在的类必须重写hashCode()和equals()方法
* String作为Map的键--key
* key-value是搭配关系,通过key找到对应的value
*
* Map的实现类 HashMap、TreeMap、LinkedHashMap、Hashtable、Properties
*
* 添加、删除、修改
* put(key,value) 将指定的key-value添加或修改到当前map对象中
* putAll(Map m) 将m中所有的key-value对存放到当前map中
* remove(key) 删除指定的key-value对,并返回value
* clear() 清空当前map的所有数据
* 查询
* get(key) 通过key获取对应的value
* containsKey(key) 是否包含指定的key
* containsValue(value) 是否包含指定的value
* size() 返回map中key-value对的个数
* isEmpty() 判断当前map是否为空
* equals(obj) 判断当前map和参数对象obj是否相等
* 元视图(方便)
* Set keySet() 返回所有key构成的Set集合
* Collection values() 返回所有value构成的Collection集合
* Set entrySet(): 返回所有key-value对 构成的Set集合
其实现类一:HashMap
允许使用null键和null值,与HashSet一样,不保证映射的顺序。
* 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
* 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
* 一个key-value构成一个entry
* 所有的entry构成的集合是Set:无序的、不可重复的
* HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
* HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
* JDK7以前: HashMap 是数组+链表结构
* JDK8以后: HashMap 是数组+链表+红黑树实现
HashMap底层实现原理
JDK7:
HashMap map = new HashMap()在实例化以后,底层创建了长度是16的一维数组Entry[] table,当进行添加操作时,首先,调用key所在类的hashCode方法计算key的值,这个哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。如果此位置上没有数据,那么key-value添加成功--情况1,如果此位之上有数据(意味着此位置上存放一个或多个数据(以链表形式存在))比较key和已经存在的一个或多个数据的哈希值
如果key的哈希值与已经存在的key的哈希值都不相同,此时key-value添加成功--情况2
如果key的哈希值与已经存在的某一个数据(key1,value1)的哈希值相同,则继续比较,调用key所在的类的equals(key1)方法
如果equals()返回false ,此时key-value添加成功-->情况3
如果equals()返回true,用value,替换value1
对于情况2和情况3,此时key-value和原来的数据以链表的方式存储
在不断添加的过程中,涉及到扩容问题,超出临界值默认的扩容为原来容量的2倍,并将原有的数据复制过来JDK8:
new HashMap():底层没有创建一个长度为16的数组
jdk8底层的数组是Node[],并非Entry[]
首次调用put方法时,底层才创建长度为16的数组
jdk7底层结构只有:数组+链表,jdk8中底层结构:数组+链表+红黑树
当数组的某一个索引位置上的元素以链表形式存在的数据个ew数>8 且当前数组的长度>64时,此时此索引位置上的所有数据改为使用红黑树存储
JDK7代码
static final int DEFAULT_INITIAL_CAPACITY = 16;//默认初始容量为16
static final int MAXIMUM_CAPACITY = 1 << 30;//最大支持容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
transient Entry<K,V>[] table;//创建了一个Entry[] table数组
//测试中,创建对象
HashMap map = new HashMap();//调用空参构造器
//jdk7源码
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
//空参构造器调用了带参构造器,并把,默认容量和,填充因子传进去
}
public HashMap(int initialCapacity, float loadFactor){}
//在带参构造器中,首先判断传进来容量是否小于0,如果小于抛异常,如果不小于继续判断,判断传进来的容量是否大于最大容量,如果大于,将传进来的容量改成最大容量如果不大于继续判断,判断填充因子是否小于等于0,或填充因子是否为数字,满足两个其中一个条件,则抛异常,如果不满足就继续往下运行,
int capacity = 1;
判断capacity是否小于传进来的容量,如果小于一直循环,且capacity<<=1进行位移赋值操作,循环结束
让当前类填充因子 = 传进来的填充因子
this.loadFactor = loadFactor
threshold = (int)Math.min(capacity * loadFactor,MAXIMUM_CAPACITY+1),取两个中的最小值,当做扩容的临界值
扩容的临界值 = 当前容量*填充因子
并创建一个新的Entry数组table = new Entry[capacity]
当进行put()操作时
传进来一个key,value
首先判断key是否为null。如果为null,使用putForNullKey(key)方法,添加key键(因为HashMap支持键为null)
然后通过HashMap定义的方法hash(key) 算出key的哈希值
int hash = hash(key)
然后通过HashMap定义的indexFor(hash,table.length),来计算Entry对象保存在 table中的数组索引值
然后循环,集合中没有元素,就不走循环
for(Entry<K,V> e = table[i];e!=null;e = e.next){
Object k;
//判断table[i]的hash是否和添加的key的哈希值一样,并且(两个key也相等或key.equals(k))判断两个是因为,key的键可以为任何类型,使用==为了判断基本数据类型,使用equals为了判断引用数据类型
if(e.hash == hash && ((k = e.key) == key || key.equals(k)){
V oldValue = e.value//如果相等,就把已经存在的值改为,新添加的值,因为HashMap的key不能相等.添加相同的key也就意味着替换value
e.value = value
return oldValue//替换成功之后,把旧值返回出去
}
}
//循环完或没走循环
modCount++;//用于记录扩容和结构改变的次数
addEntry(hash,key,value,i)//把key的哈希值,key,value,和索引位置,传进去
return null
}
//然后看addEntry方法
void addEntry(int hash,K key V value,int bucketIndex){
//判断集合中元素的个数是否大于等于扩容的临界值并且,集合中buckteIndex位置的元素不能为空
if((size >= threshold) && (null != table[bucketIndex])){
//如果满足条件,则去扩容
resize(2*table.length);//扩容,并创建数组
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//如果不满足if直接创建或走完if语句创建
createEntry(hash,key,value,bucketIndex)
}
void createEntry(int hash,K key,V value,int bucketIndex){
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash,key,value,e)
size++;//集合中的个数++
}
void resize(int newCapacity){
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果旧数组的容量== 最大支持容量
if(oldCapacity == MAXIMUN_CAPACITY){
threshold = Integer.MAX_VALUE;//就让扩容的临界值=Integer包装类中最大的值
return;//并返回
}
//如果不满足条件
Entry[] newTable = new Entry[newCapacity]//创建一个新的Entry数组
transfer(newTable,rehash)//将原有节点的hash在扩容后的数组中重新确定下标,并将所有元素存入新建的数组中
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1)//取新的临界值
}
JDK8代码
//默认设置了几个静态常量
static final int DEFAULT_INITIAL_CAPACITY = 1<<4; // 默认容量为16
static final int MAXIMUM_CAPACITY = 1 << 30;//支持的最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//填充因子
static final int TREEIFY_THRESHOLD = 8;//Bucket中链表长度大于该默认值,转换为红黑树
static final int UNTREEIFY_THRESHOLD = 6;//Bucket中红黑树存储的Node小于该默认值,转换为链表
//创建的是Node[]数组,因为Node{}实现类Map.Entry,所以本质上还是Entry数组//空参构造器
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//put方法
public V put(K key,V value){
return putVal(hash(Key),key,value,false,true);
//调用putVal()添加,hash(key)获取key的哈希值
}
final V putVal(int hash,K key,V value,boolean onlyIfAbsent,boolean evict){
Node<K,V>[] tab; Node<K,V> p;int n,i;
//判断是否是第一次添加
if((tab = table) == null || (n =tab.length)==0){
//如果是第一次添加则
n = (tab = resize()).length;//resize()返回新的Node数组
}
//p赋值为tab索引位置下元素,并判断该位置是否有元素,也就是,是否为空
if((p = tab[i = (n-1) & hash])== null)
//如果为空则直接添加成功
tab[i] = newNode(hash,key,value,null);
else{
//如果p不为空,也就意味着该索引位置上有元素
Node<K,V> e; K k;
//如果传入的key和已经有的key的hash值一样,就替换一下
if(p.hash == hash && ((k = p.key) == key || (key !=null && key.equals(k)))){
e = p;
}else if(p instanceof TreeNode){//判断当前p是否是TreeNode类型的数据,如果是,则采用这种方法添加
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
}else{
//都不满足以上条件,继续使用链表
//遍历链表数据
for(int binCount = 0;;++binCount){
if((e = p.next) == null){
p.next = newNode(hash,key,value,null)
//判断binCount是否大于等于TREEIFY_THRESHOLD-1,如果大于了,执行treeifyBin方法
if (binCount >= TREEIFY_THRESHOLD - 1)
//这个方法是将链表转换为红黑树
treeifyBin(tab, hash);
break;
}
if(e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();//扩容
afterNodeInsertion(evict);
return null;
}
HashMap的子类:LinkedHashMap
LinkedHashMap
* 在HashMap的基础上,使用了一对双向链表来记录添加元素的顺序
Entry<K,V> before,after;
* 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代
* 顺序:迭代顺序与 Key-Value 对的插入顺序一致
TreeMap
TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序,TreeMap 可以保证所有的 Key-Value 对处于有序状态。
* TreeSet底层使用红黑树结构存储数据
* TreeMap 的 Key 的排序:
* 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有TreeMap的Key应该是同一个类的对象,否则将会抛出类转换异常 ClassCastException
* 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要实现Comparable 接口
* TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
Hashtable
Hashtable是个古老的Map实现类,JDK1.0就提供了,不同于HashMap,Hashtable,该实现类是线程安全的
* Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
* 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
* 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
* Hashtable判断两个key相等、两个value相等的标准,与HashMap一致
Hashtable的子类:properties
Properties
*Properties 类是 Hashtable 的子类,该对象用于处理配置文件
* 由于配置文件里的 key、value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型
*存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
collections工具类的使用
Collections 是一个操作 Set、List 和 Map 等集合的工具类
*
* Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
*
*排序操作:(均为static方法)
* reverse(List) 反转List中元素的顺序
* shuffle(List) 对List集合元素进行随机排序
* sort(List) 根据元素的自然顺序对执行的List集合元素按升序排序
* sort(List,Comparator) 根据制定的Comparator产生的顺序对List集合元素进行排序
* swap(List list,int i,int j) 将指定List集合中i处元素和j处元素进行交换
* 查找、替换
* Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
* Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
* Object min(Collection)
* Object min(Collection,Comparator)
* int frequency(Collection,Object):返回指定集合中指定元素的出现次数
* boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List 对象的所有旧值
*
* Collections常用方法:同步控制
* Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
本人是Java初学者,水平有限,本文章中如果有不对的地方,麻烦您能指出来。向您表示感谢。