Java 中有几种常用的数据结构,主要分为 Collection 和 map 两个主要接口(接口只提供方法,并不提供实现),而程序中最终使用的数据结构是继承自这些接口的数据结构类。
List(接口)
List 是有序的 Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在 List 中的位置,类似于数组下 >标)来访问 List 中的元素,这类似于 Java 的数组。
Vector
基于数组(Array)的 List,其实就是封装了数组所不具备的一些功能方便我们使用,所以它难易避免数组的限制,同时性能也不可能超越数组。所以,在可能的情况下,我们要多运用数组。另外很重要的一点就是 Vector 是线程同步的(sychronized)的,这也是 Vector 和 ArrayList 的一个的重要区别。
ArrayList
同 Vector 一样是一个基于数组上的链表,但是不同的是 ArrayList 不是同步的。所以在性能上要比
Vector 好一些,但是当运行到多线程环境中时,可需要自己在管理线程的同步问题。
LinkedList
LinkedList 不同于前面两种 List,它不是基于数组的,所以不受数组性能的限制。
它每一个节点(Node)都包含两方面的内容:
1.节点本身的数据(data);
2.下一个节点的信息(nextNode)。
所以当对 LinkedList 做添加,删除动作的时候就不用像基于数组的 ArrayList 一样,必须进行大量的数据移动。只要更改 nextNode 的相关信息就可以实现了,这是 LinkedList 的优势。
List 总结
所有的 List 中只能容纳单个不同类型的对象组成的表,而不是 Key-Value 键值对。例如:[ tom,1,c ] 所有的 List 中可以有相同的元素,例如 Vector 中可以有 [ tom,koo,too,koo ] 所有的 List 中可以有 null 元素,例如[ tom,null,1 ]
基于 Array 的 List(Vector,ArrayList)适合查询,而 LinkedList 适合添加,删除操作
Set(接口)
Set 是不包含重复元素的 Collection
HashSet
虽然 Set 同 List 都实现了 Collection 接口,但是他们的实现方式却大不一样。List 基本上都是以 Array 为基础。但是 Set 则是在 HashMap 的基础上来实现的,这个就是 Set 和 List 的根本区别。HashSet 的存储方式是把 HashMap 中的 Key 作为 Set 的对应存储项。看看 HashSet 的 add(Object obj)方法的实现就可以一目了然了。
LinkedHashSet
HashSet 的一个子类,一个链表。
SortedSet
有序的 Set,通过 SortedMap 来实现的。
Map(接口)
Map 是一种把键对象和值对象进行关联的容器,而一个值对象又可以是一个 Map,依次类推,这样就可形成一个多级映射。对于键对象来说,像 Set 一样,一个 Map 容器中的键对象不允许重复,这是为了保持查找结果的一致性;如果有两个键对象一样,那你想得到那个键对象所对应的值对象时就有问题了,可能你得到的并不是你想的那个值对象,结果会造成混乱,所以键的唯一性很重要,也是符合集合的性质的。当然在使用过程中,某个键所对应的值对象可能会发生变化,这时会按照最后一次修改的值对象与键对应。对于值对象则没有唯一性的要求,你可以将任意多个键都映射到一个值对象上,这不会发生任何问题(不过对你的使用却可能会造成不便,你不知道你得到的到底是那一个键所对应的值对象)。
HashMap
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。
(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。另外,HashMap 是非线程安全的,也就是说在多线程的环境下,可能会存在问题,而 Hashtable 是线程安全的。
TreeMap
TreeMap 则是对键按序存放,
HashTable
Hashtable 是一个散列表,它存储的内容是键值对(key-value)映射。Hashtable 继承于 Dictionary,实现了 Map、Cloneable、java.io.Serializable 接口。Hashtable 的函数都是同步的,这意味着它是线程安全的。它的 key、value 都不可以为 null。java 并发集合
数据结构(Data Structure)是编程中的基本元素,几乎每个程序都使用了一种或多种数据结构来存储和管理数据。java API 提供了包含接口、类和算法的 java 集合框架,它实现了可用在程序中的大量数据结构。
当需要在并发程序中使用数据集合时,必须要谨慎地选择相应的实现方式。大多数集合类并不能直接用于并发应用,因为他们没有对本身数据的并发访问进行控制,如果一些并发任务共享一个不适用于病发任务的数据结构,将会遇到数据不一致性的错误,并将影响程序的正确运行,这类数据结构的一个例子就是
ArrayList。
java 提供了一些可以用于并发程序中的数据集合,他们不会引起任何问题,一般来说,java 提供了两类适用于并发应用的集合:
(1)阻塞式集合(blocking collection):这类集合包括添加和移除数据的方法。当集合已满或者为空时,被调用的添加或者 移除方法就不能立即执行,那么调用这个饭方法的线程将被阻塞,一直到该方法可以被成功执行。(2)非阻塞式集合(Non-blocking collection):这类集合也包括添加和移除数据的方法。如果集合已满或者为空时,在调用添加或者移除方法时会返回 null 或者抛出异常,但是调用这个方法的线程不会被阻塞。
以下就是 java 常用的并发集合:
非阻塞式列表对应的实现类:ConcurrentLinkedDeque
阻塞式列表对应的实现类:LinkedBlockingDeque 用于数据生成或者消费的阻塞式列表对应的实现类:LinkedTransferQueue 按优先级排序列表元素的阻塞式列表对应的实现类:PriorityBlockingQueue
带有延迟列表元素的阻塞式列表对应的实现类:DelayQueue
非阻塞式列表可遍历映射对应的饿实现类:ConcurrentSkipListMap 随机数字对应的实现类:ThreadLockRandom
原子变量对应的实现类:AtomicLong 和 AtomicIntegerArray
集合类:
集合类存放于 java.util 包中。
集合类存放的都是对象的引用,而非对象本身,出于表达上的便利,我们称集合中的对象就是指集合中对象的引用(reference)。
集合类型主要有 3 种:set(集)、list(列表)和 map(映射)。
总的说来,Java API 中所用的集合类,都是实现了 Collection 接口,他的一个类继承结构如下:
Collection<--List<--Vector
Collection<--List<--ArrayList
Collection<--List<--LinkedList
Collection<--Set<--HashSet
Collection<--Set<--HashSet<--LinkedHashSet
Collection<--Set<--SortedSet<--TreeSet
集合框架:集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。
对外的接口:集合的抽象数据结构。接口允许我们独立地操纵集合而不用考虑集合的具体实现。接口的实现:接口的具体实现类。从本质上来讲,它们是可重用的数据结构。集合运算算法 :在实现了集合接口的对象上执行有用的计算,比如排序和搜索,的方法。算法是多态的,同名的方法可以被任何合适的接口实现类调用,从本质上来讲,算法是可重用的功能 。5、容器类介绍以及之间的区别(容器类估计很多人没听这个词,Java 容器主要可以划分为 4 个部分:List 列表、Set 集合、Map 映射、工具类(Iterator 迭代器、Enumeration 枚举类、Arrays 和 Collections),具体的可以看看这篇博文 Java 容器类)
一、List接口
List 是一个继承于 Collection 的接口,即 List 是集合中的一种。List 是有序的队列,List 中的每一个元素都有一个索引;第一个元素的索引值是 0,往后的元素的索引值依次+1。和 S et 不同,List 中允许有重复的元素。实现 List 接口的集合主要有:ArrayList、LinkedList、
Vector、Stack。
ArrayList
ArrayList 是一个动态数组,也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括 null。每一个 ArrayList 都有一个初始容量:
private static final int DEFAULT_CAPACITY = 10;
随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。
size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。
ArrayList 擅长于随机访问。同时 ArrayList 是非同步的。
LinkedList
同样实现 List 接口的 LinkedList 与 ArrayList 不同,ArrayList 是一个动态数组,而 LinkedLi st 是一个双向链表。所以它除了有 ArrayList 的基本操作方法外还额外提供了 get,remove, insert 方法在 LinkedList 的首部或尾部。
由于实现的方式不同,LinkedList 不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端,节约一半时间)。这样做的好处就是可以通过较低的代价在 List 中进行插入和删除操作。
与 ArrayList 一样,LinkedList 也是非同步的。如果多个线程同时访问一个 List,则必须自己实现访问同步。一种解决方法是在创建 List 时构造一个同步的 List:
List list = Collections.synchronizedList(new LinkedList(…));
Vector
与 ArrayList 相似,但是 Vector 是同步的。所以说 Vector 是线程安全的动态数组。它的操作与 ArrayList 几乎一样。
Stack
Stack 继承自 Vector,实现一个后进先出的堆栈。Stack 提供 5 个额外的方法使得 Vector 得以被当作堆栈使用。基本的 push 和 pop 方法,还有 peek 方法得到栈顶的元素,empty 方法测试堆栈是否为空,search 方法检测一个元素在堆栈中的位置。Stack 刚创建后是空栈。
二、Set接口
Set 是一个继承于 Collection 的接口,Set 是一种不包括重复元素的 Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与 List 一样,它同样运行 null 的存在但是仅
有一个。由于 Set 接口的特殊性,所有传入 Set 集合中的元素都必须不同,关于 API 方面。
Set 的 API 和 Collection 完全一样。实现了 Set 接口的集合有:HashSet、TreeSet、Linke dHashSet、EnumSet。
HashSet
HashSet 堪称查询速度最快的集合,因为其内部是以 HashCode 来实现的。集合元素可以是 null,但只能放入一个 null。它内部元素的顺序是由哈希码来决定的,所以它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。
TreeSet
TreeSet 是二叉树实现的,基于 TreeMap,生成一个总是处于排序状态的 set,内部以 Tre eMap 来实现,不允许放入 null 值。它是使用元素的自然顺序对元素进行排序,或者根据创建 Set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。
LinkedHashSet
LinkedHashSet 集合同样是根据元素的 hashCode 值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet 将会以元素的添加顺序访问集合的元素。LinkedHashSet
在迭代访问 Set 中的全部元素时,性能比 HashSet 好,但是插入时性能稍微逊色于 HashS et。
三、Map接口
Map 与 List、Set 接口不同,它是由一系列键值对组成的集合,提供了 key 到 Value 的映射。在 Map 中它保证了 key 与 value 之间的一一对应关系。也就是说一个 key 对应一个 value,所以它不能存在相同的 key 值,当然 value 值可以相同。实现 map 的集合有:HashMap、
HashTable、TreeMap、WeakHashMap。
HashMap
以哈希表数据结构实现,查找对象时通过哈希函数计算其位置,它是为快速查询而设计的,其内部定义了一个 hash 表数组(Entry[] table),元素会通过哈希转换函数将元素的哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,可能通过查看 HashMap.Entry 的源码它是一个单链表结构。
HashTable
也是以哈希表数据结构实现的,解决冲突时与HashMap也一样也是采用了散列链表的形式。
HashTable 继承 Dictionary 类,实现 Map 接口。其中 Dictionary 类是任何可将键映射到相应值的类(如 Hashtable)的抽象父类。每个键和每个值都是一个对象。在任何一个 Dicti onary 对象中,每个键至多与一个值相关联。Map 是”key-value 键值对”接口。 HashTabl e 采用”拉链法”实现哈希表不过性能比 HashMap 要低。
TreeMap
有序散列表,实现 SortedMap 接口,底层通过红黑树实现。
WeakHashMap
谈 WeakHashMap 前先看一下 Java 中的引用(强度依次递减)
强引用:普遍对象声明的引用,存在便不会 GC软引用:有用但并非必须,发生内存溢出前,二次回收弱引用:只能生存到下次 GC 之前,无论是否内存足够虚引用:唯一目的是在这个对象被 GC 时能收到一个系统通知以弱键实现的基于哈希表的 Map。在 WeakHashMap 中,当某个键不再正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除,因此,该类的行为与其他的 Map 实现有所不同。null 值和 null 键都被支持。
该类具有与 HashMap 类相似的性能特征,并具有相同的效能参数初始容量和加载因子。像大多数集合类一样,该类是不同步的。
四、总结
List、Set 都是继承自 Collection 接口,Map 则不是List 特点:元素有放入顺序,元素可重复 ,Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在 set 中的位置是有该元素的 HashCode 决定的,其位置其实是固定的,加入 Set 的 Object 必须定义 equals()方法 ,另外 list 支持 for 循环,也就是通过下标来遍历,也可以用迭代器,但是 set 只能用迭代,因为他无序,无法用下标来取得想要的值。)Set 和 List 对比:Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List 可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
Map 适合储存键值对的数据线程安全集合类与非线程安全集合类 : LinkedList、ArrayList、HashSet 是非线程安全的,Vector 是线程安全的;HashMap 是非线程安全的,HashTable 是线程安全的;StringBuilder 是非线程安全的,StringBuffer 是线程安全的。集合(自己补齐)
1:
Collection(单列集合)
List(有序,可重复)
ArrayList 底层数据结构是数组,查询快,增删慢线程不安全,效率高 Vector 底层数据结构是数
组,查询快,增删慢线程安全,效率低 LinkedList 底层数据结构是链表,查询慢,增删快线程不安
全,效率高 Set(无序,唯一)
HashSet 底层数据结构是哈希表。哈希表依赖两个方法:hashCode()和 equals()执行顺序:
首先判断 hashCode()值是否相同是:继续执行 equals(),看其返回值是 true:说明元素重复,
不添加是 false:就直接添加到集合否:就直接添加到集合最终:自动生成 hashCode()和
equals()即可 LinkedHashSet 底层数据结构由链表和哈希表组成
。
由链表保证元素有序
。
由
哈希表保证元素唯一。
TreeSet 底层数据结构是红黑树。(是一种自平衡的二叉树)如何保证元素唯一性呢?
根据比较的返回值是否是 0 来决定如何保证元素的排序呢?两种方式自然排序(元素具
备比较性)让元素所属的类实现 Comparable 接口比较器排序(集合具备比较性)让集合接收
一个 Comparator 的实现类对象 Map(双列集合)A:Map 集合的数据结构仅仅针对键有效,与
值无关。B:存储的是键值对形式的元素,键唯一,值可重复。
HashMap 底层数据结构是哈希表。线程不安全,效率高哈希表依赖两个方法:
hashCode()和 equals()执行顺序:首先判断 hashCode()值是否相同是:继续执行
equals(),看其返回值是 true:说明元素重复,不添加是 false:就直接添加到集合否:就直接添
加到集合最终:自动生成 hashCode()和 equals()即可 LinkedHashMap 底层数据结构由链
表和哈希表组成。由链表保证元素有序。由哈希表保证元素唯一。
Hashtable 底层数据结构是哈希表。线程安全,效率低哈希表依赖两个方法:
hashCode()和 equals()执行顺序:首先判断 hashCode()值是否相同是:继续执行
equals(),看其返回值是 true:说明元素重复,不添加是 false:就直接添加到集合否:就直接添
加到集合最终:自动生成 hashCode()和 equals()即可 TreeMap 底层数据结构是红黑树。(是
一种自平衡的二叉树)如何保证元素唯一性呢?根据比较的返回值是否是
0
来决定如何保证
元素的排序呢?两种方式自然排序(元素具备比较性)让元素所属的类实现 Comparable 接口
比较器排序(集合具备比较性)让集合接收一个 Comparator 的实现类对象
到底使用那种集合(自己补齐)看需求。
2:
是否是键值对象形式:是:Map 键是否需要排序:是:TreeMap 否:HashMap 不知道,
就使用 HashMap。否:Collection 元素是否唯一:是:Set 元素是否需要排序:是:TreeSet
否:HashSet 不知道,就使用 HashSet 否:
List 要安全吗:是:Vector(其实我们也不用
它,后面我们讲解了多线程以后,我在给你回顾用谁) 否:ArrayList 或者 LinkedList 增删多:
LinkedList 查询多:ArrayList 不知道,就使用 ArrayList 不知道,就使用 ArrayList3:集合的常见方法及遍历方式 Collection:add()remove()contains()iterator()size()遍历:增强 for 迭代
器|--Listget()遍历:普通
for|--SetMap:put()remove()containskey(),containsValue()keySet()get()value()entrySet()siz
e()遍历:根据键找值根据键值对对象分别找键和值作业:我讲解过的任意一个集合,我要
求你存储什么,你就能够存储什么。并且,还要能够遍历出来。
4:ArrayList,LinkedList,HashSet,HashMap(掌握)存储字符串和自定义对象数据并遍历
5:
集
合的嵌套遍历(理解)
①HashMap 的工作原理
HashMap 基于 hashing 原理,我们通过 put()和 get()方法储存和获取对象。当我们将键值对传递给 put()方法时,它调用键对象的 hashCode()方法来计算 hashcode,让后找到 bucket 位置来储存值对象。当获取对象时,通过键对象的 equals()方法找到正确的键值对,然后返回值对象。HashMap 使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap 在每个链表节点中储存键值对对象。
当两个不同的键对象的 hashcode 相同时会发生什么? 它们会储存在同一个 bucket 位置的链表中。键对象的 equals()方法用来找到键值对。
因为 HashMap 的好处非常多,我曾经在电子商务的应用中使用 HashMap 作为缓存。因为金融领域非常多的运用 Java,也出于性能的考虑,我们会经常用到
HashMap 和 ConcurrentHashMap。
②HashMap 和 Hashtable 的区别
HashMap 和 Hashtable 都实现了 Map 接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。
. HashMap 几乎可以等价于 Hashtable,除了 HashMap 是非 synchronized 的,并可以接受 null(HashMap 可以接受为 null 的键值(key)和值(value),而 Hashtable 则不行)。
. HashMap 是非 synchronized,而 Hashtable 是 synchronized,这意味着 Hashtable 是线程安全的,多个线程可以共享一个 Hashtable;而如果没有正确的同步的话,多个线程是不能共享 HashMap 的。Java 5 提供了 ConcurrentHashMap,它是 HashTable 的替代,比 HashTable 的扩展性更好。
. 另一个区别是 HashMap 的迭代器 (Iterator)是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。所以当有其它线程改变了 HashMap 的结构(增加或者移除元素),将会抛出 ConcurrentModificationException,但迭代器本身的 remove()方法移除元素则不会抛出 ConcurrentModificationException 异常。但这并不是一个一定发生的行为,要看 JVM。这条同样也是 Enumeration 和 Iterator 的区别。
. 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。如果你不需要同步,只需要单一线程,那么使用 HashMap 性能要好过 Hashtable。
. HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
要注意的一些重要术语:
sychronized 意味着在一次仅有一个线程能够更改 Hashtable。就是说任何线程要更新 Hashtable 时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新 Hashtable。Fail-safe 和 iterator 迭代器相关。如果某个集合对象创建了 Iterator 或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出
ConcurrentModificationException 异常。但其它线程可以通过 set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用 set()方法,将会抛出 IllegalArgumentException 异常。
结构上的更改指的是删除或者插入一个元素,这样会影响到 map 的结构。我们能否让 HashMap 同步?
HashMap 可以通过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);
结论
Hashtable 和 HashMap 有几个主要的不同:线程安全以及速度。仅在你需要完全
的线程安全的时候使用 Hashtable,而如果你使用 Java 5 或以上的话,请使用
ConcurrentHashMap 吧。
ashMap 和 HashSet 的区别是 Java 面试中最常被问到的问题。如果没有涉及到
Collection 框架以及多线程的面试,可以说是不完整。而 Collection 框架的问题不涉及到 HashSet 和 HashMap,也可以说是不完整。HashMap 和 HashSet 都是
collection 框架的一部分,它们让我们能够使用对象的集合。collection 框架有自己的接口和实现,主要分为 Set 接口,List 接口和 Queue 接口。它们有各自的特点,Set 的集合里不允许对象有重复的值,List 允许有重复,它对集合中的对象进行索引,Queue 的工作原理是 FCFS 算法(First Come, First Serve)。
首先让我们来看看什么是 HashMap 和 HashSet,然后再来比较它们之间的分别。
③HashMap 和 HashSet 的区别
HashMap 和 HashSet 的区别是 Java 面试中最常被问到的问题。如果没有涉及到
Collection 框架以及多线程的面试,可以说是不完整。而 Collection 框架的问题不涉及到 HashSet 和 HashMap,也可以说是不完整。HashMap 和 HashSet 都是
collection 框架的一部分,它们让我们能够使用对象的集合。collection 框架有自己的接口和实现,主要分为 Set 接口,List 接口和 Queue 接口。它们有各自的特点,Set 的集合里不允许对象有重复的值,List 允许有重复,它对集合中的对象进行索引,Queue 的工作原理是 FCFS 算法(First Come, First Serve)。
首先让我们来看看什么是 HashMap 和 HashSet,然后再来比较它们之间的分别。
什么是 HashSet
HashSet 实现了 Set 接口,它不允许集合中有重复的值,当我们提到 HashSet 时,第一件事情就是在将对象存储在 HashSet 之前,要先确保对象重写 equals()和
hashCode()方法,这样才能比较对象的值是否相等,以确保 set 中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。
public boolean add(Object o)方法用来在 Set 中添加元素,当元素值重复时则会立即返回 false,如果成功添加的话会返回 true。
什么是 HashMap
HashMap 实现了 Map 接口,Map 接口对键值对进行映射。Map 中不允许重复的键。Map 接口有两个基本的实现,HashMap 和 TreeMap。TreeMap 保存了对象的排列次序,而 HashMap 则不能。HashMap 允许键和值为 null。HashMap 是非 synchronized 的,但 collection 框架提供方法能保证 HashMap synchronized,这样多个线程同时访问 HashMap 时,能保证只有一个线程更改 Map。
public Object put(Object Key,Object value)方法用来将元素添加到 map 中。
HashSet 和 HashMap 的区别
*HashMap* *HashSet* HashMap 实现了 Map 接口 HashSet 实现了 Set 接口
HashMap 储存键值对 HashSet 仅仅存储对象 使用 put()方法将元素放入 map 中使用 add()方法将元素放入 set 中 HashMap 中使用键对象来计算 hashcode 值
HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回 false HashMap 比较快,因为是使用唯一的键来获取对象 HashSet 较 HashMap 来说比较慢
同学 1:嗯...数组+链表同学 2:数组+链表... 同学 3:数组+链表...
同学 4:数组+链表+红黑树...
同学 n:.....
为什么答案会有两种?难道大家学习的 HashMap 有两个版本?我突然想起马克思哲学里面的一句话,真理是相对的,不是绝对的,变化才是唯一的真理。不错,对于 Java 这种语言排行榜经常排于榜首的高级语言,变化也是它的生存之道。Java 在推出新版本的同时,不断的完善重要 class 的数据结构,提升它的性能,稳固它的安全性。HashMap 就是其中之一。
HashMap 在 Java1.7 里使用的是数组+链表的数据结构,在 Java1.8 里使用的是数组+链表+红黑树。
HashMap 特点
HashMap 是基于哈希表的 Map 接口实现。HashMap 底层采用的是 Entry 数组和链表实现。HashMap 是采用 key-value 形式存储,其中 key 是可以允许为 null 但是只能是一个,并且 key 不允许重复(如果重复则新值覆盖旧值)。HashMap 是线程不安全的。HashMap 存入的顺序和遍历的顺序有可能是不一致的。
HashMap 保存数据的时候通过计算 key 的 hash 值来去决定存储的位置。
HashMap 源代码
要看 HashMap 的源代码,我们还是从 HashMap 的构造方法开始一步一步的讲解。
小总结:可以看出 HashMap 构造的时候会初始化 16 个容量,并且负载因子是 0.75。负载因子是什么呢?我们后面讲。
小总结:这个构造方法没有什么可说的,只是多了些验证。在这里呢?构造方法就算初始化完毕了。
我们知道 HashMap 最常用的方法也就是 put 方法了,那么下面我们就着重去探究一下 put 方法的实现原理,也就是对 HashMap 的一个透彻理解。
这段代码好好的研读,请仔细往下看:
第一步:直接判断 table==EMPTY_TABLE ,那么这个 table 是什么呢?看下图:
这个 Entry 是 Map 的一个静态内部类,里面最重要的属性有 key、value 和 next 三个属性值,在这里,我想大家已经猜到了,这个 key 和 value 是不是我们 put 的时候的 key 和 value 呢?答案是的,这个 next 又是干嘛用的呢?实际上这个 Entry 的的数据结构是一个单链表,这个 next 的属性的值还是这个 Entry,表示的是当前的节点的下一个节点是哪个 Entry。
好啦,源代码看到这里,我们知道,在 put 方法中,直接判断 table 是否为 null,那么很显然到目前为止我们的 table 肯定是为 null 的,那么继续看如果 table 为 null 则要执行的代码。看下图:
哇塞,可以很直观的看到,我们实际上是初始化了一个 Entry 数组,而我们
HashMap 中的数据都是保存在了 Entry[]里面了。
小总结:HashMap 其实就是一个线性的 Entry 数组,而在 Entry 这个对象中保存了 key 和 value,至于 Entry 对象中的 next 的具体作用是干嘛的,稍等做介绍哦。
第二步:判断 key 是否为 null。从这里可以看出,当判断 key 如果为 null 的话,
并没有抛出什么异常错误,很显然 HashMap 是支持 key 为 null 的。那么就来看看 key 如果为 null,会怎么处理呢?
小总结:首先去循环遍历这个 Entry 数组,判断是否有 key 为 null 的情况,如果有则新值覆盖掉旧值。如果没有 key 为 null 的情况,则 hash 值为 0,数据存储在这个 Entry 数组的第 0 个位置,也就是 table[0],具体方法可以查看 addEntry 方法,在这里呢,我就不再演示了。
第三步:通过 hash 方法对 key 进行计算 hash 散列值,并且根据这个散列值查找这个要保存的值应该存储到 table 这个数组中的哪个索引位置。
小总结:当去变量这个 Entry 数组的时候,去判断两个 Entry 对象的 key 的 hash 是否相同则仅仅表示它们的存储位置是相同的,然后继续判断两个 Entry 的 key 是否相等或者 equals 是否相等,如果条件都满足,则表示要添加的元素,key 已经重复,则直接将新值覆盖掉旧值,并且 return 返回,一旦条件不满足,则直接将添加的元素添加到 Entry 对象中。
HashMap 和 HashSet 是 Java Collection Framework 的两个重要成员,其中 HashMap 是 Map 接口的常用实现类,HashSet 是 Set 接口的常用实现类。虽然 HashMap 和 HashSet 实现的接口规范不同,但它们底层的 Hash 存储机制完全一样,甚至 HashSet 本身就采用 HashMap 来实现的。
实际上,HashSet 和 HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算法决定集合元素的存储位置,这样可以保证能快速存、取集合元素;对于 HashMap 而言,系统 key-value 当成一个整体进行处理,系统总是根据 Hash 算法来计算 key-value 的存储位置,这样可以保证能快速存、取 Map 的 key-value 对。
在介绍集合存储之前需要指出一点:虽然集合号称存储的是 Java 对象,但实际上并不会真正将 Java 对象放入 Set 集合中,只是在 Set 集合中保留这些对象的引用而言。也就是说:Java 集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的 Java 对象。
集合和引用
就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并不是真正的把 Java 对象放入数组中,只是把对象的引用放入数组中,每个数组元素都是一个引用变量。
HashMap 的存储实现
上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的
value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:hash(),这个方法是一个纯粹的数学计算,其方法如下:
这个方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置——而 HashMap 底层数组的长度总是 2 的 n 次方,这一点可参看后面关于 HashMap 构造器的介绍。
当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。
根据上面 put 方法的源代码可以看出,当程序试图将一个 key-value 对放入 HashMap 中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。
当向 HashMap 中添加 key-value 对,由其 key 的 hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false)。
上面程序中还调用了 addEntry(hash, key, value, i); 代码,其中 addEntry 是 HashMap 提供的一个包访问权限的方法,该方法仅用于添加一个 key-value 对。下面是该方法的代码:
threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。从上面程序中②号代码可以看出,当 size++ >= threshold 时, HashMap 会自动调用 resize 方法扩充 HashMap 的容量。每扩充一次,HashMap 的容量就增大一倍。
上面程序中使用的 table 其实就是一个普通数组,每个数组都有一个固定的长度,这个数组的长度就是 HashMap 的容量。HashMap 包含如下几个构造器:
HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。当创建一个 HashMap 时,系统会自动创建一个 table 数组来保存 HashMap 中的 Entry,下面是 HashMap 中一个构造器的代码:
程序①号代码处可以看到:table 的实质就是一个数组,一个长度为 capacity 的数组。
对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。
无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个
Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是: HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。如图 1 所示:
图 1. HashMap 的存储示意 HashMap 的读取实现
当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry — —也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,在根据该 hashCode 返回值找出该 key 在 table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。看 HashMap 类的 get(K key) 方法代码:
< br " />如果开始就知道 HashMap 会保存多个 key-value 对,可以在创建时就使用较大的初始化容量,如果 HashMap 中 Entry 的数量一直不会超过极限容量(capacity * load factor),HashMap 就无需调用 resize() 方法重新分配 table 数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为 capacity 的 Entry 数组),因此创建 HashMap 时初始化容量设置也需要小心对待。
1.HashMap 中的主要变量:
Node<k,v>[] table; //桶数组 ,Node<K,V>类型的数组,里面的元素是链表,用于存放 HashMap 元素的实体 size; //记录键值对个数 loadFactor ; //负载因子 threshold ; //阈值,决定了 HashMap 何时扩容
2.主要的方法原理:
构造方法。在 JDK 源码中当我们自定义 HashMap 初始容量大小时,构造函数并非直接把我们定义的数值当做 HashMap 容量大小,而是把该数值当做参数调用方法 tableSizeFor,然后把返回值作为 HashMap 的初始容量大小。自己手写的则省略
此代码看到一篇博客讲解的比较好:
https://blog.csdn.net/fan2012huan/article/details/51097331
put 方法。主要来存储数据元素到数组中。主要流程为:
在存储中还要考虑到扩容问题:随着 HashMap 中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响 HashMap 的速度,为了保证 HashMap 的效率,系统必须要在某个临界点进行扩容处理。
所谓的扩容就是,重新构建一个新的数组,遍历旧的数组取出所有元素,再装到新的数组中去。(!!重新计算每个元素在数组中的位置再装到新数组中去。
即:重新计算每个数组哈希码(因为数组长度变了,哈希码也变了。))
JDK 1.6 当数量大于容量 * 负载因子即会扩充容量。(使用此方法)
JDK 1.7 初次扩充为:当数量大于容量时扩充;第二次及以后为:当数量大于容量 * 负载因子时扩充。
JDK 1.8 初次扩充为:与负载因子无关;第二次及以后为:与负载因子有关。
其详细计算过程需要具体详解。原来旧的数组复制进去。
(3)get 方法和 put 的方法比较类似。 获得外界传入的 key 值后,再通过算法得到哈希码,然后就去桶数组找就行了。
(4)其他方法原理相对比较简单省略。
【1】提供了更好的写并发能力,降低了对读一致性的要求。
允许多个修改操作并发进行,关键在于使用了锁分离技术。它使用多个锁来控制对 hash 表的不同部分的修改。concurrenthashmap 内部使用 segment 来表示这些不同的部分。每个段其实都是一个晓得 hashtable。他们有自己的锁。
【2】需要跨段的方法 size()。containsValue(),他们可能需要锁住整张表而不仅仅是某某个段。
这需要按照顺序锁定所有的段,操作完毕后。又按照顺序释放完所有锁的段。这里“按顺序”是很重要的,否则极有可能出现死锁。在 concurrenthashmap 内部,段数组是 final 的。并且其成员变量都是 final 的
【3】Segment 继承了 ReentrantLock 表明每个 segment 都可以当作一个锁,
(ReentrantLock 前文已经提到)则有对每个 segment 中数据的同步操作的,都是是用每个 segment 容器对象自身的锁来实现。
ArrayMap 与 HashMap 比较?优缺点?应用场景?
比较:
ArrayMap 采用的数据结构是两个一维数组,而 HashMap 使用的是一维数组和单链表数据结构
ArrayMap 默认容量为 0,而 HashMap 默认容量是 16ArrayMap 没有最大容量的限制,直到报 oom,而 HashMap 最大容量最大是 Integer.MAX_VALUEArrayMap 默认每次扩容时原来容量一半的增量;而 HashMap 默认每次扩容时原来容量 0.75 倍的增量优点:
相比 HashMap 内存空间更优,因为比 HashMap 少了一个实体类进行装饰容量为 4 或者 8 时又缓存复用功能扩容比 HashMap 高效,因为 HashMap 扩容时相当于重构,需要重新重新计算 hash 值和移动元素;而 ArrayMap 扩容时只需拷贝缺点:
数据量大的情况下查询效率比 HashMap 差存取效率比 HashMap 低,因为每次存取都需要二分法找到对应的下标没有实现 Serializable,不便在 Android bundle 进行传输使用场景:
数据量小,建议百量级别内存要求高
}
二话不说,上来先丢了三个构造函数。从构造函数中,我们可以获取到这些信息:Hashtable 默认的初始化容量为 11(与 HashMap 不同),负载因子默认
为 0.75(与 HashMap 相同)。而正因为默认初始化容量的不同,同时也没有
对容量做调整的策略,所以可以先推断出,Hashtable 使用的哈希函数跟
HashMap 是不一样的(事实也确实如此)。
跟 HashMap 相比,Hashtable 的 get 方法非常简单。我们首先可以看见 get 方
法使用了 synchronized 来修饰,所以它能保证线程安全。并且它是通过链表的
方式来处理冲突的。另外,我们还可以看见 HashTable 并没有像 HashMap 那
样封装一个哈希函数,而是直接把哈希函数写在了方法中。而哈希函数也是比较简单的,它仅对哈希表的长度进行了取模。
put 方法一开始就表明了不能有 null 值,否则就会向你抛出一个空指针异常。
Hashtable的put方法也是使用synchronized来修饰。你可以发现,在Hashtable
中,几乎所有的方法都使用了 synchronized 来保证线程安全。
remove 方法我已经不想加注释了,跟 get 和 put 的原理差不多。如果看过上
一篇的 HashMap 的话,或者理解了上面的 put 方法的话,我相信 remove 方法看一眼就能懂了。
Hashtable 的 rehash 方法相当于 HashMap 的 resize 方法。跟 HashMap 那种
巧妙的 rehash 方式相比,Hashtable 的 rehash 过程需要对每个键值对都重新
计算哈希值,而比起异或和与操作,取模是一个非常耗时的操作,所以这也是导致效率较低的原因之一。
1.共同点:都是双列集合,底层都是哈希算法
2.区别:
1.HashMap是线程不安全的,效率高,JDK1.2版本Hashtable是线程安全的,效率低,JDK1.0版本2.HashMap可以存储null键和null值Hashtable不可以存储null键和null值3.代码示例:
HashMap 与 Hashtable 的区别是面试中经常遇到的一个问题。这个问题看似简单,但如果深究进去,也能了解到不少知识。本文对两者从来源,特性,算法等多个方面进行对比总结。力争多角度,全方位的展示二者的不同,做到此问题的终结版。
继承的父类不同HashMap 和 Hashtable 不仅作者不同,而且连父类也是不一样的。HashMap 是继承自 AbstractMap 类,而 HashTable 是继承自 Dictionary 类。不过它们都实现了同时实现了 map、Cloneable(可复制)、Serializable(可序列化)这三个接口
Dictionary 类是一个已经被废弃的类(见其源码中的注释)。父类都被废弃,自然而然也没人用它的子类 Hashtable 了。
NOTE: This class is obsolete. New implementations shouldimplement the Map interface, rather than extending this class.对外提供的接口不同Hashtable 比 HashMap 多提供了 elments() 和 contains() 两个方法。
elments() 方法继承自 Hashtable 的父类 Dictionnary。elements() 方法用于返回此 Hashtable 中的 value 的枚举。 contains()方法判断该 Hashtable 是否包含传入的 value。它的作用与 containsValue() 一致。事实上,contansValue() 就只是调用了一下 contains() 方法。
线程安全性不同
Hashtable 是线程安全的,它的每个方法中都加入了 Synchronize 方法。在多线程并发的环境下,可以直接使用 Hashtable,不需要自己为它的方法实现同步
HashMap 不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。具体的原因在下一篇文章中会详细进行分析。使用 HashMap 时就必须要自己增加同步处理,
虽然 HashMap 不是线程安全的,但是它的效率会比 Hashtable 要好很多。这样设计是合理的。在我们的日常使用当中,大部分时间是单线程操作的。HashMap
把这部分操作解放出来了。当需要多线程操作的时候可以使用线程安全的
ConcurrentHashMap。ConcurrentHashMap 虽然也是线程安全的,但是它的效率比 Hashtable 要高好多倍。因为 ConcurrentHashMap 使用了分段锁,并不对整个数据进行锁定。
遍历方式的内部实现上不同
Hashtable、HashMap 都使用了 Iterator。而由于历史原因,Hashtable 还使用了
Enumeration 的方式 。
HashMap 的 Iterator 是 fail-fast 迭代器。当有其它线程改变了 HashMap 的结构(增加,删除,修改元素),将会抛出 ConcurrentModificationException。不过,通过 Iterator 的 remove()方法移除元素则不会抛出 ConcurrentModificationException 异常。但这并不是一个一定发生的行为,要看 JVM。
JDK8 之前的版本中,Hashtable 是没有 fast-fail 机制的。在 JDK8 及以后的版本中 ,
HashTable 也是使用 fast-fail 的, 源码如下:
modCount 的使用类似于并发编程中的 CAS(Compare and Swap)技术。我们可以看到这个方法中,每次在发生增删改的时候都会出现 modCount++的动作。而 modcount 可以理解为是当前 hashtable 的状态。每发生一次操作,状态就向前走一步。设置这个状态,主要是由于 hashtable 等容器类在迭代时,判断数据是否过时时使用的。尽管 hashtable 采用了原生的同步锁来保护数据安全。但是在出现迭代数据的时候,则无法保证边迭代,边正确操作。于是使用这个值来标记状态。一旦在迭代的过程中状态发生了改变,则会快速抛出一个异常,终止迭代行为。
初始容量大小和每次扩充容量大小的不同
Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
创建时,如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。也就是说 Hashtable 会尽量使用素数、奇数。而 HashMap 则总是使用 2 的幂作为哈希表的大小。
之所以会有这样的不同,是因为 Hashtable 和 HashMap 设计时的侧重点不同。
Hashtable 的侧重点是哈希的结果更加均匀,使得哈希冲突减少。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。而 HashMap 则更加关注 hash 的计算效率问题。在取模计算时,如果模数是 2 的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。HashMap 为了加快 hash 的速度,将
哈希表的大小固定为了 2 的幂。当然这引入了哈希分布不均匀的问题,所以
HashMap 为解决这问题,又对 hash 算法做了一些改动。这从而导致了 Hashtable 和 HashMap 的计算 hash 值的方法不同
18、HashMap 与 HashSet 的区别
1 HashMap和HashSet的区别是Java面试中最常被问到的问题。如果没有涉及到Collection框架以及多线程的面试,可以说是不完整。而Collection框架的问题不涉及到HashSet和HashMap,也可以说是不完整。HashMap和HashSet都是collection框架的一部分,它们让我们能够使用对象的集合。collection框架有自己的接口和实现,主要分为Set接口,List接口和Queue接口。它们有各自的特点,Set的集合里不允许对象有重复的值,List允许有重复,它对集合中的对象进行索引,Queue的工作原理是FCFS算法(FirstCome,FirstServe)。
2 HashSet实现了Set接口,它不允许集合中有重复的值,当我们提到HashSet 时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals() 和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。
3
public boolean add(Object o)方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。什么是HashMap4 HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。HashMap允许键和值为null。HashMap是非synchronized的,但collection框架提供方法能保证HashMap synchronized,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。
5 public Object put(Object Key,Object value)方法用来将元素添加到map 中。
6
你可以阅读这篇文章看看HashMap的工作原理,以及这篇文章看看HashMap 和HashTable的区别。HashSet和HashMap的区别*HashMap* *HashSet*HashMap实现了Map接口 HashSet实现了Set接口HashMap储存键值对 HashSet仅仅存储对象使用put()方法将元素放入map中 使用add()方法将元素放入set中7 HashMap中使用键对象来计算hashcode值 HashSet使用成员对象来计算 hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
8 HashMap比较快,因为是使用唯一的键来获取对象 HashSet较HashMap 来说比较慢
HashMap 和 HashSet 的区别是 Java 面试中最常被问到的问题。如果没有涉及到 Collection 框架以及多线程的面试,可以说是不完整。而 Collection 框架的问题不涉及到 HashSet 和 HashMap,也可以说是不完整。HashMap 和 HashSet 都是 collection 框架的一部分,它们让我们能够使用对象的集合。collection 框架有自己的接口和实现,主要分为 Set 接口,List 接口和 Queue 接口。它们有各自的特点,Set 的集合里不允许对象有重复的值,List 允许有重复,它对集合中的对象进行索引,Queue 的工作原理是 FCFS 算法(First Come, FirstServe)。
首先让我们来看看什么是 HashMap 和 HashSet,然后再来比较它们之间的分别。
什么是HashSet
HashSet 实现了 Set 接口,它不允许集合中有重复的值,当我们提到 HashSet 时,第一件事情就是在将对象存储在 HashSet 之前,要先确保对象重写 equals()和 hashCode()方法,这样才能比较对象的值是否相等,以确保 set 中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。
public boolean add(Object o)方法用来在 Set 中添加元素,当元素值重复时则会立即返回 false,如果成功添加的话会返回 true。
什么是HashMap
HashMap 实现了 Map 接口,Map 接口对键值对进行映射。Map 中不允许重复的键。Map
接口有两个基本的实现,HashMap 和 TreeMap。TreeMap 保存了对象的排列次序,而 HashMap 则不能。HashMap 允许键和值为 null。HashMap 是非 synchronized 的,但
collection 框架提供方法能保证 HashMap synchronized,这样多个线程同时访问 HashMap 时,能保证只有一个线程更改 Map。
public Object put(Object Key,Object value)方法用来将元素添加到 map 中。
你可以阅读这篇文章看看HashMap的工作原理,以及这篇文章看看HashMap和HashTable 的区别。
HashSet和HashMap的区别
19、HashSet 与 HashMap 怎么判断集合元素重复?
HashMap 中判断元素是否相同主要有两个方法,一个是判断 key 是否相同,一个是判断 value 是否相同
HashSet 不能添加重复的元素,当调用 add(Object)方法时候,首先会调用 Object 的 hashCode 方法判 hashCode 是否已经存在,如不存在则直接插入元素;
如果已存在则调用 Object 对象的 equals 方法判断是否返回 true,如果为 true 则说明元素已经存在,如为 false 则插入元素。
发现 HashSet 竟然是借助 HashMap 来实现的,利用 HashMap 中 Key 的唯一性,来保证 HashSet 中不出现重复值。
从这段代码中可以看出,HashMap 中的 Key 是根据对象的 hashCode() 和 euqals()来判断是否唯一的。
结论:为了保证 HashSet 中的对象不会出现重复值,在被存放元素的类中必须要重写 hashCode()和 equals()这两个方法。
重写 hashcode()和 equles()方法
可以看到在遍历 table 中的元素判断键和值,
1,如果 hash 码值不相同,说明是一个新元素,存储;
如果没有元素和传入对象(也就是 add 的元素)的 hash 值相等,那么就认为这个元素在 table 中不存在,将其添加进 table;
2.1,如果 hash 码值相同,且 equles 判断相等,说明元素已经存在,不存;
2.2,如果 hash 码值相同,且 equles 判断不相等,说明元素不存在,存;如果有元素和传入对象的 hash 值相等,那么,继续进行 equles()判断,如果仍然相等,那么就认为传入元素已经存在,不再添加,结束,否则仍然添加;
可见 hashcode()和 equles()在此显得很关键了,下面就来了解一下 hashcode 和 equles 这两个方法:
首先要明确:只通过 hash 码值来判断两个对象时否相同合适吗?答案肯定是不合适的,因为存在两个元素的 hash 码值相同但是并不是同一个元素这样的情况;
那么要问什么是 hash 码值?
在 java 中存在一种 hash 表结构,它通过一个算法,计算出的结果就是 hash 码值;这个算法叫 hash 算法;
hash 算法是怎么计算的呢?
是通过对象中的成员来计算出来的结果;
如果成员变量是基本数据类型的值, 那么用这个值 直接参与计算;如果成员变量是引用数据类型的值,那么获取到这个成员变量的哈希码值后,再参数计算
因此在 hashSet 的 add 方法添加元素时,仅仅依靠 hash 值判断是否存在是不完全的 还要依靠 equals 方法。
ArryList 初始化时,elementData 数组大小默认为 10;
每次 add()时,先调用 ensureCapacity()保证数组不会溢出,如果此时已满,会扩展为数组 length 的 1.5 倍+1,然后用 array.copy 的方法,将原数组拷贝到新的数组中;
ArrayList 线程不安全,Vector 方法是同步的,线程安全;
LinkedList 是基于双链表实现的:Object element;
Entry next,previous;
初始化时,有个 header Entry,值为 null;
使用 header 的优点是:在任何一个条目(包括第一个和最后一个)都有一个前置条目和一个后置条目,因此在 LinkedList 对象的开始或者末尾进行插入操作没有特殊的地方;
使用场景:
. 如果应用程序对各个索引位置的元素进行大量的存取或删除操作,ArrayList 对象要远优于 LinkedList 对象;
. 如果应用程序主要是对列表进行循环,并且循环时候进行插入或者删除操作,
LinkedList 对象要远优于 ArrayList 对象
数组(Array)
一、数组特点:所谓数组,就是相同数据类型的元素按一定顺序排列的集合;数组的存储区间是连续的,占用内存比较大,故空间复杂的很大。但数组的二分查找时间复杂度小,都是 O(1);数组的特点是:查询简单,增加和删除困难;
1.1 在内存中,数组是一块连续的区域
1.2 数组需要预留空间
在使用前需要提前申请所占内存的大小,如果提前不知道需要的空间大小时,预先申请就可能会浪费内存空间,即数组的空间利用率较低。注:数组的空间在编译阶段就需要进行确定,所以需要提前给出数组空间的大小(在运行阶段是不允许改变的)
1.3 在数组起始位置处,插入数据和删除数据效率低。
插入数据时,待插入位置的元素和他后面的所有元素都需要向后搬移删除数据时,待删除位置后面的所有元素都需要向前搬移。
1.4 随机访问效率很高,时间复杂度可以达到 O(1)
因为数组的内存是连续的,想要访问那个元素,直接从数组的首地址向后偏移就可以访问到了。
1.5 数组开辟的空间,在不够使用的时候需要进行扩容;扩容的话,就涉及到需要把旧数组中的所有元素向新数组中搬移。
1.6 数组的空间是从栈分配的。(栈:先进后出)二、数组的优点:随机访问性强,查找速度快,时间复杂度是 0(1)三、数组的缺点:
3.1 从头部删除、从头部插入的效率低,时间复杂度是 o(n),因为需要相应的向前搬移和向后搬移。
3.2 空间利用率不高
3.3 内存空间要求高,必须要有足够的连续的内存空间。
3.4 数组的空间大小是固定的,不能进行动态扩展。
链表(ListNode)
一、链表的特点:所谓链表,链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到 O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n)的时间,而线性表和顺序表相应的时间复杂度分别是
O(logn)和 O(1)。
链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达 O(N)。链表的特点是:查询相对于数组困难,增加和删除容易。
1.1 在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续。
1.2 链表中的元素有两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址。
每一个数据都会保存下一个数据的内存地址,通过该地址就可以找到下一个数据
1.3 查找数据时间效率低,时间复杂度是 o(n)
因为链表的空间是分散的,所以不具有随机访问性,如果需要访问某个位置的数据,需要从第一个数开始找起,依次往后遍历,知道找到待查询的位置,故可能在查找某个元素时,时间复杂度是 o(n)
1.4 空间不需要提前指定大小,是动态申请的,根据需求动态的申请和删除内存空间,扩展方便,故空间的利用率较高
1.5 任意位置插入元素和删除元素时间效率较高,时间复杂度是 o(1)
1.6 链表的空间是从堆中分配的。(堆:先进先出,后进后出)二、链表的优点
2.1 任意位置插入元素和删除元素的速度快,时间复杂度是 o(1)
2.2 内存利用率高,不会浪费内存
2.3 链表的空间大小不固定,可以动态拓展。
三、链表的缺点随机访问效率低,时间复杂度是 o(1)
总之:对于想要快速访问数据,不经常有插入和删除元素的时候,选择数组对于需要经常的插入和删除元素,而对访问元素时的效率没有很高要求的话,选择链表
补充 range()和 arange()函数的区别:
range(start, end, step),返回一个 list 对象也就是 range.object,起始值为 start,终止值为 end,但不含终止值,步长为 step。只能创建 int 型 list。
arange(start, end, step),与 range()类似,也不含终止值。但是返回一个 array 对象。需要导入 numpy 模块(import numpy as np 或者 from numpy import*),并且 arange 可以使用 float 型数据。Q1: 数组和链表取最大值的时间复杂度;
A1: 取最大值实际上就是对数组和链表分别进行访问,则取最大值的时间复杂度分别是:数组 O(1),链表 O(n)
Q2: 数组和链表的底层是用什么写的?
A2:数组的底层是 Array List,链表的底层是 Hashmap.
Q3: hasmap 冲突和溢出的解释和处理方法:
A3:
Hash 函数:
非哈希表的特点:关键字在表中的位置和它之间不存在一个确定的关系,查找的过程为给定值一次和各个关键字进行比较,查找的效率取决于和给定值进行比较的次数。
哈希表的特点:关键字在表中位置和它之间存在一种确定的关系
哈希函数:一般情况下,需要在关键字与它在表中的存储位置建立一个函数关系,以 f(key)作为关键字为 key 的记录在表中的位置,通常称这个函数 f(key)为哈希函数。
hash:(散列)就是把任意长度的输入,通过散列算法,变成固定程度的输出,该输出就是散列值。这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。 简单的说就是一种将任意长度的消息压缩到莫伊固定长度的消息摘要的函数。
hash 冲突:就是根据 key 即经过一个函数 f(key)得到的结果的作为地址去存放当前的 key value 键值对(这个是 hashmap 的存值方式),但是却发现算出来的地址上已经有人先来了。就是说这个地方被抢了啦。这就是所谓的 hash 冲突啦。
二、数据结构的运算。
数据结构研究的意义:一般认为,一个数据结构是由数据元素依据某种逻辑联系组织起来的。对数据元素间逻辑关系的描述称为数据的逻辑结构;数据必须在计算机内存储,数据的存储结构是数据结构的实现形式,是其在计算机内的表示;此外讨论一个数据结构必须同时讨论在该类数据上执行的运算才有意义。一个逻辑数据结构可以有多种存储结构,且各种存储结构影响数据处理的效率。
在许多类型的程序的设计中,数据结构的选择是一个基本的设计考虑因素。许多大型系统的构造经验表明,系统实现的困难程度和系统构造的质量都严重的依赖于是否选择了最优的数据结构。许多时候,确定了数据结构后,算法就容易得到了。有些时候事情也会反过来,我们根据特定算法来选择数据结构与之适应。不论哪种情况,选择合适的数据结构都是非常重要的。
选择了数据结构,算法也随之确定,是数据而不是算法是系统构造的关键因素。这种洞见导致了许多种软件设计方法和程序设计语言的出现,面向对象的程序设计语言就是其中之一。
结构分类:数据结构是指同一数据元素类中各数据元素之间存在的关系。数据结构分别为逻辑结构、存储结构(物理结构)和数据的运算。数据的逻辑结构是从具体问题抽象出来的数学模型,是描述数据元素及其关系的数学特性的,有时就把逻辑结构简称为数据结构。逻辑结构是在计算机存储中的映像,形式地定义为(K,R)(或
(D,S)),其中,K 是数据元素的有限集,R 是 K 上的关系的有限集。
根据数据元素间关系的不同特性,通常有下列四类基本的结构:
⑴集合结构。该结构的数据元素间的关系是“属于同一个集合”。
⑵线性结构。该结构的数据元素之间存在着一对一的关系。
⑶树型结构。该结构的数据元素之间存在着一对多的关系。
⑷图形结构。该结构的数据元素之间存在着多对多的关系,也称网状结构。
从上面所介绍的数据结构的概念中可以知道,一个数据结构有两个要素。一个是数据元素的集合,另一个是关系的集合。在形式上,数据结构通常可以采用一个二元组来表示。
数据结构的形式定义为:数据结构是一个二元组 :Data_Structure=(D,R),其中,D 是数据元素的有限集,R 是 D 上关系的有限集。线性结构的特点是数据元素之间是一种线性关系,数据元素“一个接一个的排列”。在一个线性表中数据元素的类型是相同的,或者说线性表是由同一类型的数据元素构成的线性结构。在实际问题中线性表的例子是很多的,如学生情况信息表是一个线性表:表中数据元素的类型为学生类型; 一个字符串也是一个线性表:表中数据元素的类型为字符型,等等。
线性表是最简单、最基本、也是最常用的一种线性结构。 线性表是具有相同数据类型的 n(n>=0)个数据元素的有限序列,通常记为: (a1,a2,… ai-1,ai, ai+1,…an) ,其中 n 为表长, n=0 时称为空表。 它有两种存储方法:顺序存储和链式存储,它的主要基本操作是插入、删除和检索等。
数据结构在计算机中的表示(映像)称为数据的物理(存储)结构。它包括数据元素的表示和关系的表示。数据元素之间的关系有两种不同的表示方法:顺序映象和非顺序映象,并由此得到两种不同的存储结构:顺序存储结构和链式存储结构。顺序存储方法:它是把逻辑上相邻的结点存储在物理位置相邻的存储单元里,结点间的逻辑关系由存储单元的邻接关系来体现,由此得到的存储表示称为顺序存储结构。顺序存储结构是一种最基本的存储表示方法,通常借助于程序设计语言中的数组来实现。
链接存储方法:它不要求逻辑上相邻的结点在物理位置上亦相邻,结点间的逻辑关系是由附加的指针字段表示的。由此得到的存储表示称为链式存储结构,链式存储结构通常借助于程序设计语言中的指针类型来实现索引存储方法:除建立存储结点信息外,还建立附加的索引表来标识结点的地址。
散列存储方法:就是根据结点的关键字直接计算出该结点的存储地址。
数据结构中,逻辑上(逻辑结构:数据元素之间的逻辑关系)可以把数据结构分成线性结构和非线性结构。线性结构的顺序存储结构是一种顺序存取的存储结构,线性表的链式存储结构是一种随机存取的存储结构。线性表若采用链式存储表示时所有结点之间的存储单元地址可连续可不连续。逻辑结构与数据元素本身的形式、内容、相对位置、所含结点个数都无关。
常用的数据结构:数组
在程序设计中,为了处理方便, 把具有相同类型的若干变量按有序的形式组织起来。这些按序排列的同类数据元素的集合称为数组。在 C 语言中, 数组属于构造数据类型。一个数组可以分解为多个数组元素,这些数组元素可以是基本数据类型或是构造类型。因此按数组元素的类型不同,数组又可分为数值数组、字符数组、指针数组、结构数组等各种类别。
栈
是只能在某一端插入和删除的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
队列
一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列是按照“先进先出”或“后进后出”的原则组织数据的。队列中没有元素时,称为空队列。
链表
是一种物理存储单元上非连续、非顺序的存储结构,它既可以表示线性结构,也可以用于表示非线性结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
树
是包含 n(n>0)个结点的有穷集合 K,且在 K 中定义了一个关系 N,N 满足 以下条件:
(1)有且仅有一个结点 K0,他对于关系 N 来说没有前驱,称 K0 为树的根结点。
简称为根(root)。 (2)除 K0 外,K 中的每个结点,对于关系 N 来说有且仅有一个前驱。
(3)K 中各结点,对关系 N 来说可以有 m 个后继(m>=0)。
图
图是由结点的有穷集合 V 和边的集合 E 组成。其中,为了与树形结构加以区别,在图结构中常常将结点称为顶点,边是顶点的有序偶对,若两个顶点之间存在一条边,就表示这两个顶点具有相邻关系。
堆
在计算机科学中,堆是一种特殊的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。
散列表
若结构中存在关键字和 K 相等的记录,则必定在 f(K)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系 f 为散列函数(Hash function),按这个思想建立的表为散列表
23、堆的结构
堆就是用数组实现的二叉树,所以它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。
堆的常用方法:
构建优先队列支持堆排序快速找出一个集合中的最小值(或者最大值)在朋友面前装逼堆属性堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。
根据这一属性,那么最大堆总是将其中的最大值存放在树的根节点。而对于最小堆,根节点中的元素总是树中的最小值。堆属性非常有用,因为堆常常被当做优先队列使用,因为可以快速地访问到“最重要”的元素。
注意:堆的根节点中存放的是最大或者最小元素,但是其他节点的排序顺序是未知的。例如,在一个最大堆中,最大的那一个元素总是位于 index 0 的位置,但是最小的元素则未必是最后一个元素。--唯一能够保证的是最小的元素是一个叶节点,但是不确定是哪一个。
堆并不能取代二叉搜索树,它们之间有相似之处也有一些不同。我们来看一下两者的主要差别:
节点的顺序。在二叉搜索树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。
内存占用。普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配内存。堆仅仅使用一个数据来存储数组,且不使用指针。
平衡。二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到
O(log n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足堆属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证 O(log n) 的性能。
搜索。在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入、删除操作。
1.栈内存存储的是局部变量而堆内存存储的是实体;
2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。
浅拷贝就是成员数据之间的一一赋值23 :把值赋给一一赋给要拷贝的5261 值4102 。但是可能会有这样的情况:对象还包含资源,这里的资源可以值堆资源,或者一个文1653 件。。当值拷贝的时候,两个对象就有用共同的资源,同时对资源可以访问,这样就会出问题。深拷贝就是用来解决这样的问题的,它把资源也赋值一次,使对象拥有不同的资源,但资源的内容是一样的。对于堆资源来说,就是在开辟一片堆内存,把原来的内容拷贝。
如果你拷贝的对象中引用了某个外部的内容(比如分配在堆上的数据),那么在拷贝这个对象的时候,让新旧两个对象指向同一个外部的内容,就是浅拷贝;如果在拷贝这个对象的时候为新对象制作了外部对象的独立拷贝,就是深拷贝引用和指针的语义是相似的,引用是不可改变的指针,指针是可以改变的引用。
其实都是实现了引用语义。
private Node revert(Node head) {
Node pre = null;
Node next;
while (head!=null){ next = head.next; head.next = pre; pre = head;
head = next;
}
return pre;
}