最常用的、最简单的数据结构,它是n个数据元素的有限序列、
实现线性表:输出存储线性表元素,即是用一组连续的存储单元,依次存储线性表数据元素,另一种是使用链表存储线性表元素,用一组任意的存储单元存储线性表的数据元素(存储单元可以连续,可以不连续)。
先进后出
一段添加元素。另一端取出元素。入队出队。使用场景:因为队列先进先出的特点,在多线程阻塞队列管理中非常适用。
物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个节点,一个是存储元素的数据域(存储空间),另外一个是指向下一个节点的指针域。根据指针的指向,链表形成不同的结构,例如单链表、双链表,循环链表
链表优点:不需要初始化容量,可以任意加减元素,添加或删除元素只需要改变前后两个元素节点的指针域指向地址即可,所以添加,删除很快
缺点:含有大量指针域,占用空间大,需要查找元素需要遍历链表来查找,比较耗时。
使用场景:数据量小,需要频繁增加,删除的操作。
一种数据结构,由n(n>=1)个有限节点组成的具有层级关系的集合。它看起来像倒挂的树,根朝上,叶朝下,具有以下特点:
每个节点有零个或多个子节点
没有父节点的节点是根节点
每一个非根节有且只有一个父节点
除了根节点,没格子节点可以分为多个不想交的子树。
二叉树,是树中特殊的一种:
每个节点最多有两颗子树,节点的度最多为2
左子树和右子树是有顺序的,次序不能颠倒。
即使某个节点只有一个子树,也要区分左右子树。
二叉树是折中的方案,添加删除元素很快,在查找方面也有自己的算法优化,所以二叉树基友链表的好处,也有数组的好处,是两者的优化方法,在处理大批量动态数据方面非常有用。
拓展:平衡二叉树、红黑树。B+树等。这些数据结构二叉树的基础上衍生了很多功能,在实际应用中广泛用到,例如Mysql索引结构用的是B+树,还有HashMap底层源码是红黑树
目录
数据结构:
Collection接口和Map接口区别:
HashMap 的工作原理是什么?
HashMap的寻址方式
HashSet 的工作原理是什么?
Collection(源自于java.util.Collection)和Map是java集合框架中两个基本的集合类型,要区别不统集合,首先要从Collection和Map开始。Collection接口是传统的集合接口,可以把单个对象存储起来,而Map接口是映射接口,存储的是键值对。
Collection | |||||
List | Set | ||||
LinkList | ArrayList | Vector | HashSet | TreeSet | LinkedHashSet |
Map | |||
HashMap | HashTable | LinkedHashMap | TreeMap |
List(interface)是一种链表, 有序可重复 List可以通过index指导元素的位置,允许重复元素,ArrayList(允许所有元素包括null)
LinkedList(双向链表结构)
Vector可以实现List接口。类似于ArrayList,但是Vector是同步的,由Vector创建的Iterator,虽然和ArrayList是同一接口,但是因为Vector是同步的,当一个Iterator(迭代器)被创建而且正在被使用时,另一个线程改变了Vector的状态(例如删除一些元素),这时调用Iterator方法将抛出ConcurrentModificationException(同事发生),因此异常必须捕获。
Vector是线程安全的,因为Vector好多方法是sychornized关键字修饰的,比如addElement方法:
Public syschronized void addElement(E obj){
modCount++;
ensureCapatityHelper(elementCount+1);
elementData[elementCount++]=obj;
}
这样在多线程并发访问Vector实例的时候,保证某一刻最多只有一个线程调用Vector中被syschornized修饰的方法。
反观List,在ArrayList中的add方法:
Public Boolean add(E e){
ensureCapacityInternal (size+1);
elementData[size++]=e;
reture true;
}
有可能同一时候有一个以上线程访问该方法。
我们知道size++运算过程不是原子操作,有可能被中断。那么如果在size++过程中被中断,而另外一个线程恰好执行了一次这样的方法,就会造成脏数据产生。
拓展:什么是原子操作?
原子操作(atomic operation)是不需要syschronized(同步),这是多线程编程老生常谈了,所谓原子操作指的是·不会被线程调度机制打断的操作,这样操作一开始,就一直运行,一直运行到结束,中间不会有任何context switch 切换到另外一个线程。通常所说的原子操作包括对非long和double型的primitive进行赋值,以及返回这两者之外的primitive。之所以要把它们排除在外是因为它们都比较大,而JVM的设计规范又没有要求读操作和赋值操作必须是原子操作(JVM可以试着去这么作,但并不保证)。
如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,这个操作就是原子操作。
原子操作可以是个步骤,也可以是多个不走,但是其顺序不可以被打乱,也不能被切割,只执行其中一部分。将整个操作作为一个整体是原子性的核心特征。
在单处理器系统中,能够在单条指令中完成的操作就可以认为原子操作,因为中断只能发生在指令之间,这也是CPU指令系统引入的test_and_set / test_and clear等指令用于临界资源互斥的原因,但是在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统有多个处理器在独立运行,即使在单条指令中完成操作也有可能受到干扰。
如果你代码进程中有多个进程在同时进行,而这些线程可能同时会同时运行这段代码,如果运行结果和单线程运行的结果是一样的,而且其他的变量也和预期值一样,是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
比如上例中一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了
也就是说像HashTable和Vector这样的容器实现了线程安全,是通过同步关键字实现的。
此外,我们又注意到,不管是Vector还是List容器,都有可能会出现ConCurrenceModificationException的异常。这是因为几乎所有的集合类都有快速失败的(Fail-Fast)校验机制。这是为了确保集合方法一致而设立的保护措施。他的实现原理就是modCount修改计数器。如在遍历列表的时候(使用迭代器的方式),这时候会保存当前的modCount值,当每次迭代器取值的时候,会通过checkForComodification()方法来校验modCount是否发生了改变。如果发生了改变,就会抛出ConCurrenceModificationException的异常。
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
那么,即使是使用Vector仍可能会抛出这种异常,这是不是和它声称的线程安全所相悖呢?
其实这里的异常和线程同步是两码事。因为使用迭代器遍历容器的时候,这是记录了modCount值,然后调用了remove方法,这在单线程中本来就是不允许的,和多线程同步没有关系。
线程同步是为了实现线程安全,而在Vector中则是实现了线程的部分安全。
线程安全性不是一个非真即假的命题。 Vector 的方法都是同步的,并且 Vector 明确地设计为在多线程环境中工作。但是它的线程安全性是有限制的,即在某些方法之间有状态依赖(类似地,如果在迭代过程中 Vector 被其他线程修改,那么由 Vector.iterator() 返回的 iterator会抛出ConcurrentModifiicationException。
Set(interface)无序不可重复,HashSet,LinkedHashSet,TreeSet可以实现set接口。Set是一种不包含重复元素的Collection,即任意两个元素e1和e2都是有e1.equals(e2)=false, set最多有一个null元素。因此set的构造函数有一个约束条件,传入的Collection参数不能包含重复元素。但是必须小心操作可变对像(Mutable Object).如果一个set的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。
Map接口:也是接口,但是没有继承collection接口,描述了从不重复的键到值的映射,键值对,
HashMap散列表,基于哈希表实现,就是键值对的映射关系,元素顺序不固定,适合对元素插入删除定位等操作。
TreeMap:红黑树实现,TreeMap的元素保持着某种固定的顺序,更加适合对元素的遍历。
Iterator接口:所有实现Iterator接口方法能以迭代方式逐个访问集合中的各元素,并可以从Collection中除去适当的元素。
Iterator it=a.iterator();
while (it.hasNext()){
String ob=(String)it.next();
System.out.println(ob);
}.
Hsahtable类:继承Map接口,实现key-value的哈希表。HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
Comparable接口:可用于比较的实现,可以实现compareTo方法。Entity层实现Comparabel接口
====原理
HashMap是非线程安全的。而HashMap的线程不安全主要体现在resize时的死循环
我们知道在 Java 中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap 也是如此。实际上 HashMap 是一个**“链表散列”**。
HashMap 是基于 hashing 的原理。
HashMap 图解
public static int highestOneBit(int i) {
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
? 当两个对象的 hashCode 相同会发生什么?
因为 hashcode 相同,所以它们的 bucket 位置相同,“碰撞”会发生。
因为 HashMap 使用链表存储对象,这个 Entry(包含有键值对的 Map.Entry 对象)会存储在链表中。
? hashCode 和 equals 方法有何重要性?
HashMap 使用 key 对象的 #hashCode() 和 #equals(Object obj) 方法去决定 key-value 对的索引。当我们试着从 HashMap 中获取值的时候,这些方法也会被用到。
同样的,所有不允许存储重复数据的集合类都使用 #hashCode() 和 #equals(Object obj) 去查找重复,所以正确实现它们非常重要。#hashCode() 和 #equals(Object obj) 方法的实现,应该遵循以下规则:
? HashMap 默认容量是多少?
默认容量都是 16 ,负载因子是 0.75 。就是当 HashMap 填充了 75% 的 busket 是就会扩容,最小的可能性是(16 * 0.75 = 12),一般为原内存的 2 倍。
? 有哪些顺序的 HashMap 实现类?
? 我们能否使用任何类作为 Map 的 key?
我们可以使用任何类作为 Map 的 key ,然而在使用它们之前,需要考虑以下几点:
比如,我有一个 类MyKey ,在 HashMap 中使用它。代码如下:
//传递给MyKey的name参数被用于equals()和hashCode()中 MyKey key = new MyKey('Pankaj'); //assume hashCode=1234 myHashMap.put(key, 'Value'); // 以下的代码会改变key的hashCode()和equals()值 key.setName('Amit'); //assume new hashCode=7890 //下面会返回null,因为HashMap会尝试查找存储同样索引的key,而key已被改变了,匹配失败,返回null myHashMap.get(new MyKey('Pankaj'));
? HashMap 的长度为什么是 2 的幂次方?
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
这个算法应该如何设计呢?我们首先可能会想到采用 % 取余的操作来实现。但是,重点来了:
这就解释了 HashMap 的长度为什么是 2 的幂次方。
HashSet 是构建在 HashMap 之上的 Set hashing 实现类。让我们直接撸下源码,代码如下:
// HashSet.java private transient HashMap map; private static final Object PRESENT = new Object();
// HashSet.java public boolean add(E e) { return map.put(e, PRESENT) == null; }
? HashSet 如何检查重复?
正如我们上面看到 HashSet 的实现原理,我们自然可以推导出,HashMap 也是如何检查重复滴。
如下摘取自 《Head First Java》 第二版:
当你把对象加入 HashSet 时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较。