标签:cti obj factory proc 不为 注意 val string 热点
Java集合前言:
集合和数组都可以对多个数据进行存储操作,简称Java容器
数组存储的特点:
1.一旦初始化,长度不可变
2.一旦数组定义好,数据类型也就确定了
3.可以存储有序可重复的数据,对于无序不可重复的需求不能满足
4.数组提供的方法有限,对于添加、删除、插入数据等操作不方便,效率也不高
5.没有现成的方法获取数组中实际有效的元素个数
而集合可以灵活的处理数组的一些缺点
单列集合,用来存储一个一个的对象
可以存储有序的可重复的数据,是动态的数组,可以存储null值
jdk1.2实现,底层数据结构:Object[] elementData.
线程不安全,但效率高,查找元素的操作方便
源码分析
(1)jdk7:
*初始化:
ArrayList list=new ArrayList();//底层创建一个长度为10的Object数组;
*添加元素:
add元素时,直接想数组的对应下标一次添加
list.add(123)===elementData[0]=new Integer(123)
*扩容:
如果此次添加元素的操作导致底层数组容量不够,则触发扩容。扩容为原来的1.5倍。并将原数组的内容复制到新数组
oldCapacity + (oldCapacity >> 1)
*注意:
尽量使用带参数的构造器,指明初始化容量,尽量避免扩容,因为扩容用到Arrays.copyOf(),这个操作代价很高
ArrayList list=new ArrayList(int capacity);
(2)jdk1.8
*初始化:
ArrayList list=new ArrayList();//底层创建一个长度为0的数组Object
*添加元素:
第一次调用add()时,底层才创建长度为10的数组,并将数据加入的数组中
list.add(123)
*扩容:
与jdk7无异
总结:
jdk7中的ArrayList的对象的创建类似于单例模式的饿汉式,
jdk8中的ArrayList的对象的创建类似于单例模式的懒汉式,延迟数组创建,节省内存
jdk 1.2实现,底层数据结构:双向链表
//节点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;
}
}
//LinkedList
内部声明了Node类型的两个属性first和last,默认为null
transient Node<E> first;
transient Node<E> last;
线程不安全,对于频繁的插入和删除操作效率较高
源码分析
*初始化:
LinkedList lis=new LinkedList();//内部声明了Node类型的两个属性first和last,默认为null
*添加元素:
list.add(123);//将123封装到Node中,创建了Node对象
其中,Node类的定义体现了LinkedList的双向链表的说法
jdk1.0实现,List接口的古老实现类,底层数据结构:Object[] elementData
线程安全,但效率不高
public synchronized boolean isEmpty() {
return elementCount == 0;
}
源码分析:
*初始化:
jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组
*扩容:
默认扩容为原来的数组长度的2倍
* 增:add(Object obj)
* 删:remove(int index)/remove(Object obj)--remove(new Integer(2))
* 改:set(int index,Object ele)
* 查:indexOf(Obj obj)/lastIndexOf(Obj obj)/get(int index)
* 插:add(int index,Object obj)
* 长度:int size();
* 遍历:
Iterator 迭代器方式(hasnext(),next()){next:先下移指针,再返回元素。开始是在第一个元素的前一位置}
foreach()
普通的循环
1.使用Vector--不建议,同步的,访问速度慢,开销大
2.Collections.synchronizedList(list)
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
3.concurrent并发包下的CopyOnWriteArrayList<>()
List<String> list = new CopyOnWriteArrayList<>();
1.读写分离:
写操作在一个复制的数组进行,读操作还是在原数组进行,互不影响
写操作需要加锁,防止并发写入时导致写入数据丢失
写操作结束后要把原始数组指向新的复制数组
2.特点:写时复制,读写分离。在写的时候,允许读操作,可以提高读操作的性能
3.适合场景:读多写少
//写操作加锁
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
final void setArray(Object[] a) { array = a; }
//读操作
@SuppressWarnings("unchecked") private E get(Object[] a, int index) { return (E) a[index]; }
缺陷:
(1)存储无序的不可重复的数据
以HashSet为例说明:
(1)无序性:不等于随机性,存储的数据在底层数组中并非按照数组索引的顺序添加, 而是根据数组的哈希值进行添加
(2)不可重复性:保证添加的元素按照equals()判断时,不能返回true
即相同的元素只能添加一个
(2)向Set中添加的数据,其所在的类一定要重写hashCode()和equals()
(3)重写的hashCode()和equals()尽可能保持一致性,相等的对象必须具有相等的散列码
底层数据结构:HashMap(),数组+链表
线程不安全,可以存储null
源码分析
添加元素的过程:以HashSet为例
(1)调用元素a所在类的hashCode()方法,计算元素a的哈希值
(2)此哈希值接着通过某种算法计算出HashSet底层数组的存放位置(即为:索引位置)
(3)判断数组此位置是否已有元素:
如果没有,则元素添加成功,---》情况1
如果有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值;
(4)如果hash值不相同,则元素a添成功---》情况2
如果hash值相同,进而需要调用元素a所在equals()方法,
(5) equals()方法返回true,元素a添加失败
equals()返回false,则元素a添加成功---》情况3
说明:
对于添加成功的情况2和3而言,元素a与已经存在指定索引位置上数据以链表的方式存储
jdk7:元素a放到数组中,指向原来的元素,链表中a在首位
jdk8:原来数组中的元素指向a,链表中a放在尾部
总结:
七上八下
是HashSet的子类,因为添加了Linked,所以遍历内部数据时 可以按照添加的顺序遍历
对于频繁的遍历操作,效率高于HashSet
源码分析:底层是LinkedHashMap
LinkedHashSet作为HashSet的子类,在添加数据的同时,
每个数据还维护两个引用,记录此数据前一个数据和后一个数据
优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet
底层数据结构:红黑树,可以按照添加元素的指定属性进行排序
源码分析
1.向TreeSet中添加的数据,要求是相同的类对象,不能添加不同类的对象
2.两种排序方式:自然排序和定制排序
3.自然排序中,比较两个对象是否相同的标准为compareTo()返回0,不再是equals()
4.定制排序中,比较两个对象是否相同的标准为compa()返回0,不再是equals()
5.构造器可以传入一个比较器做参数
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
@Test
public void test4(){
Comparator comparator=new Comparator() {
//按照年龄从小到大排列
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof Person && o2 instanceof Person) {
Person p1 = (Person) o1;
Person p2 = (Person) o2;
return Integer.compare(p1.age,p2.age);
}else {
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set1=new TreeSet(comparator);//将比较器传进去
set1.add(new Person("ZhaoMin", 20));
set1.add(new Person("WangYiBo", 22));
set1.add(new Person("XiaoZhan", 28));
set1.add(new Person("XiaoZhan", 26));
Iterator iterator=set1.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
使用的都是Collection中声明过的方法
双列集合,存储key-value数值对的数据。entry{key,value}
3.Map中的value:
无序的、可重复的,使用Collection存储所有的value
value所在的类要重写equals
4.Map中的entry:
无序的、不可重复的
使用Set存储所有的entry
Map的主要实现类,线程不安全,效率高。
底层数据结构:
1.7:数组+链表
1.8:数组+链表+红黑树
(1)jdk1.7
*初始化:
HashMap map=new HashMap();//在实例化以后,底层创建了长度为16的一维数组Entry[] table
*底层结构:
数组+链表
*put(key,value):
(1)首先调用key1所在类的hashCode()计算key1的哈希值,此哈希值经过某种算法以后,得到在数组Entry中的存放位置;
(2)如果此位置的上的数据为空,则key1-value1添加成功---情况1
(3)如果此位置上的数据不为空,(意为着在此位置已经存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
都不相同:key1-value1添加成功---情况2
和某一数据(key2-value2)相同,继续比较:调用key1所在类的equals(key2):
如果equals()返回false:key1-value1添加成功---情况3
如果equals()返回true:使用value1替换value2
补充:关于情况2、3:此时key1-value1和原来的数据以链表的形式存储
*扩容:在不断添加的过程,会涉及到扩容问题,
默认的扩容方式:当超出临界值12(且要存放放入位置非空)时, 扩容为原来容量的2倍,并将原有的数据复制过来
(2)jdk8:
*初始化:
new HashMap();//底层没有创建一个长度为16的数组
*map.put(key,value):
-首次调用put()方法创建一个长度为16的数组Node[]
-与1.7类似
-使用红黑树:
当数组中的某一个索引位置上的元素以链表形式存在的数据个数>8,且当前数组的长度>64时,此时此索引位置上的索引数据改为使用红黑树存储,提高效率,
*底层结构:
数组+链表+红黑树
*补充常量:
* EFAULT_INITIAL_CAPACITY:HashMap的默认容量:16
* DEFAULY_LOAD_FACTORY:HashMap的默认加载因子:0.75
* threshold:扩容的临界值=容量*填充因子: 16*0.75》=12
* TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
当小于该值师,又转为链表。8--符合泊松分布
红黑树使用的频率不高,到8的时候按照泊松分布,出现的概率非常小 0.000000006
* MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
* 扩容是为了尽量减少链表的使用
负载因子值得大小对HashMap有什么影响
(1)负载因子的大小决定了HashMap的数据密度
(2)负载因子越大,密度越大,越容易碰撞,数组中链表越长,造成查询、插入时比较的次数增多,性能下降
(3)负载因子越小,越容易触发扩容,数据密度越小,发生碰撞几率变小,数组中链表越短,查询和插入时比较次数越少,性能会更高。 但会浪累一定的内容空间,而且经常扩容影响性能。建议初始化时预设大一点的空间。
(4)负载因子设置为0.7-0.75,此时平均检索长度接近于常数
6.实现细节:
(1)确定桶的下标(key在数组中的索引)
①取模:hash%capacity()---性能不高,如果hash为负值,则索引也为负,不可取
②位运算:hash&(length-1)--提高性能,解决负数问题,length=2^n
hash&(length-1),length-1=11111...,所以&之后得到的数组下标肯定在
0--length-1(2^n-1)的范围内
为什么是2^n,就是为了让它-1之后得到的是一个二进制表示全为1的结果
(2)扩容--动态扩容
扩容时:capacity为原来的2倍,使用resize()扩容,需要将原数组中的键值对插入新的数组中。并重新计算桶下标
原因:在原有的HashMap底层结构基础上,添加了一对指针head和tail,维护了一个双向链表指向前一个和后一个
对于频繁的遍历操作,此类执行效率高于HashMap
LinkedHashMap 重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。
void afterNodeAccess(Node<K,V> p) {
//当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在 每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是近访问的节点,那么链表首部就是近久未 使用的节点
}
void afterNodeInsertion(boolean evict) {
//在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除晚的节点,也就是链表首部节点 ?rst
//removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现, 这在实现 LRU 的缓存中特别有用,通过移除近久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是 热点数据。
}
3.LRU缓存
添加:put(Object key,Object value)
删除:remove(Object key)\clear()
修改:put(Object key,Objec value)
查询:get(Object key)
长度:size()
遍历:keySet()/values()/entrySet()
Set set=map.keySet();
Iterator iterator=set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
//2.Collection values();//遍历所有的value
Collection collection=map.values();
for (Object obj : collection) {
System.out.println(obj);
}
//3.entrySet();//遍历所有的键值对Entry
//--方式一:
Set set1=map.entrySet();
for (Object obj : set1) {
//entrySet集合中所有的元素都是entry
Map.Entry entry=(Map.Entry)obj;
System.out.println(entry.getKey()+"-->"+entry.getValue());
}
System.out.println("====方式二====");
//--方式二:
Set set2=map.keySet();
Iterator iterator1=set2.iterator();
while (iterator1.hasNext()) {
Object key=iterator1.next();
Object value=map.get(key);
System.out.println(key+"-->"+value);
}
保证按照添加的key-value对进行排序,实现排序遍历,此时考虑key的自然排序、定制排序底层使用红黑树
向TreeMap中添加key-value,要求key必须是由同一个类创建的对象 因为要按照key进行排序:自然排序(默认)、定制排序
//定制排序
@Test
public void test1(){
Person p1 = new Person("Tom", 12);
Person p2 = new Person("Jerry", 10);
Person p3 = new Person("Lion", 15);
Person p4 = new Person("Jeff", 15);
Map map=new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof Person && o2 instanceof Person) {
Person p1 = (Person) o1;
Person p2 = (Person) o2;
return Integer.compare(p1.age,p2.age);
}
throw new RuntimeException("输入的类型不匹配");
}
});
map.put(p1, 90);
map.put(p2, 89);
map.put(p3, 96);
map.put(p4, 88);
Set entry=map.entrySet();
Iterator iterator=entry.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
(1)古老的实现类,线程安全,效率低
(2)不能存储null的key和value
常用来处理配置文件,key-value都是String类型
(1)线程安全:
HashMap线程不安全效率高,Hashtable线程安全效率低
(2)存储值:
HashMap:可以存储null的key和value
Hashtable:不能存储null的key和value
(3)元素次序:
HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
(4)HashMap 的迭代器是 fail-fast 迭代器。每次检查一下结构有没有变化,如果发生变化就抛出异常ConcurrentModi?cationException
结构发生变化是指添加或者删除至少一个元素的所有操作,或 者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
ConcurrentHashMap 和 HashMap 实现上类似,主要的差别:ConcurrentHashMap 采用了分段锁 (Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发 度更高(并发度就是 Segment 的个数)。
Segment 继承自 ReentrantLock
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor; }
final
默认的并发级别为 16,也就是说默认创建 16 个 Segment。
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
4.size操作
每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
/** * The number of elements. Accessed only either within locks * or among other volatile reads that maintain visibility. */
transient int count;
标签:cti obj factory proc 不为 注意 val string 热点
原文地址:https://blog.51cto.com/14234228/2492095