Java中数据存储方式最底层的两种结构:数组和链表,数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。而链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。有没有一种数据结构来综合一下数组和链表,以便发挥他们各自的优势?答案是肯定的!
就是:哈希表。哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”。
博客:https://blog.csdn.net/shadow_zed/article/details/78232955
1、HashSet、TreeSet
1、 存储的数据结构不同: HashSet底层用的是HashMap哈希表结构存储,而TreeSet底层用二叉树结构存储。
2、存储时保证数据唯一性依据不同:HashSet是通过复写hashCode()方法和equals()方法来保证的,而TreeSet通过Compareable接口的compareTo()方法来保证的。
3、有序性不一样:HashSet无序,可以放入null,但只能放入一个null,TreeSet有序且不允许放入null值,TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。
public static void main(String[] args) {
HashSet<String> set1 = new HashSet<String>();
set1.add("2");
set1.add("1");
set1.add("3");
set1.add(null);
set1.add(null);
System.out.println(set1);// [null, 3, 2, 1]
TreeSet<String> set2 = new TreeSet<String>();
set2.add("2");
set2.add("1");
set2.add("3");
// set2.add(null); 运行报错
System.out.println(set2);// [1, 2, 3]
}
存储原理:
1、HashSet:底层数据结构是哈希表,本质就是对哈希值的存储,通过判断元素的hashCode方法和equals方法来保证元素的唯一性,当hashCode值不相同,就直接存储了,不用在判断equals了,当hashCode值相同时,会在判断一次euqals方法的返回值是否为true,如果为true则视为用一个元素,不用存储,如果为false,这些相同哈希值不同内容的元素都存放一个桶里(当哈希表中有一个桶结构,每一个桶都有一个哈希值)
2、TreeSet:底层的数据结构是二叉树,可以对Set集合中的元素进行排序,这种结构可以提高排序性能,根据比较方法的返回值确定的,只要返回的是0就代表元素重复。
插值:当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。
简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值相等。
TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0。
LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺 序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。
LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺 序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。
public static void main(String[] args)
{
/**
* LinkedHashSet
* 底层是链表实现的,是set集合中唯一一个能保证怎么存就怎么取的集合对象
* 因为是HashSet的子类,所以也是保证元素唯一的,与HashSet的原理一样.
* HashSet的子类,LinkedHashSet也是根据元素的hashCode值来决定
* 元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是
* 以插入的顺序保存的。
*/
LinkedHashSet<String> lhs = new LinkedHashSet<String>();
lhs.add("a");
lhs.add("b");
lhs.add("a");
lhs.add("c");
lhs.add("a");
lhs.add("d");
System.out.println(lhs);//[a, b, c, d]
}
2、HashMap、TreeMap
大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap的顺序并不是HashMap放置的顺序,也就是无序。HashMap的这一缺点往往会带来困扰。
HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。HashMap最多只允许一条记录的键为Null,允许多条记录的值为 Null。TreeMap没有调优选项,因为该树总处于平衡状态。
HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。
HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()(可以重写hashCode()和equals()),为了优化HashMap空间的使用,您可以调优初始容量和负载因子。
比较:
(1)实现 : TreeMap:SortMap接口,基于红黑树 HashMap:基于哈希散列表实现
(2)存储 : TreeMap:默认按键的升序排序 HashMap:随机存储
(3)遍历 : TreeMap:Iterator遍历是排序的 HashMap:Iterator遍历是随机的
(4)性能损耗 :TreeMap:插入、删除 HashMap:基本无
(5)键值对 : TreeMap:键、值都不能为null HashMap:只允许键、值均为null
(6)安全 : TreeMap:非并发安全Map HashMap:非并发安全Map
(7)效率 : TreeMap:低 HashMap:高
HashMap通常比TreeMap快一点(树和哈希表的数据结构使然)
HashMap通过hashcode对其内容进行快速查找,而 TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。
HashMap:适用于在Map中插入、删除和定位元素。
Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
优秀博客:
https://blog.csdn.net/fujiakai/article/details/51585767
https://www.cnblogs.com/beatIteWeNerverGiveUp/p/5709841.html
http://www.importnew.com/24822.html
HashMap、HashTable区别:
HashMap是非线程安全的,HashTable是线程安全的, 内部的方法基本都是synchronized。
2、HashMap的键和值都允许有null值存在,而HashTable则不行。
3、因为线程安全的问题,HashMap效率比HashTable的要高。
HashTable、ConcurrentHashMap区别:
ConcurrentHashMap是线程安全的HashMap的实现。
同样是线程安全的类,它与HashTable在同步方面有什么不同呢?
synchronized关键字加锁的原理,其实是对对象加锁,不论你是在方法前加synchronized还是语句块前加,锁住的都是对象整体,但是ConcurrentHashMap的同步机制和这个不同,它不是加synchronized关键字,而是基于lock操作的,这样的目的是保证同步的时候,锁住的不是整个对象。事实上,ConcurrentHashMap可以满足concurrentLevel个线程并发无阻塞的操作集合对象。
HashMap实现原理:
HashMap即是采用了链地址法,也就是数组+链表的方式。
HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
实现原理:
https://www.cnblogs.com/chengxiao/p/6059914.html
https://blog.csdn.net/lyt_7cs1dn9/article/details/54925837
LinkedHashSet:
是基于HashSet实现的,在HashSet的基础上,增加了时间和空间上的开销维持了一个双向链表的关系, 保证了元素迭代的顺序。
关 注 点 | 结 论 |
LinkedHashMap是否允许空 | Key和Value都允许空 |
LinkedHashMap是否允许重复数据 | Key重复会覆盖、Value允许重复 |
LinkedHashMap是否有序 | 有序 |
LinkedHashMap是否线程安全 | 非线程安全 |
集合异同比较:https://blog.csdn.net/learningcoding/article/details/79983248
Collection接口继承树:https://www.cnblogs.com/nayitian/p/3266090.html
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.TreeMap;
import java.util.WeakHashMap;
public class Map
{
public static void main(String[] args)
{
HashMap<Integer, Integer> map1 = new HashMap<Integer, Integer>();
map1.put(3, 3);
map1.put(1, 1);
map1.put(6, 6);
map1.put(6, 6);
map1.put(2, 2);
map1.put(5, 5);
System.out.println(map1);// {1=1, 2=2, 3=3, 5=5, 6=6}
TreeMap<Integer, Integer> map2 = new TreeMap<Integer, Integer>();
map2.put(3, 3);
map2.put(1, 1);
map2.put(6, 6);
map2.put(6, 6);
map2.put(2, 2);
map2.put(5, 5);
System.out.println(map2);// {1=1, 2=2, 3=3, 5=5, 6=6}
LinkedHashMap<Integer, Integer> map3 = new LinkedHashMap<Integer, Integer>();
map3.put(3, 3);
map3.put(1, 1);
map3.put(6, 6);
map3.put(6, 6);
map3.put(2, 2);
map3.put(5, 5);
System.out.println(map3);// {3=3, 1=1, 6=6, 2=2, 5=5}
Hashtable<Integer, Integer> map4 = new Hashtable<Integer, Integer>();
map4.put(3, 3);
map4.put(1, 1);
map4.put(6, 6);
map4.put(6, 6);
map4.put(2, 2);
map4.put(5, 5);
System.out.println(map4);// {6=6, 5=5, 3=3, 2=2, 1=1}
WeakHashMap<Integer, Integer> map5 = new WeakHashMap<Integer, Integer>();
map5.put(3, 3);
map5.put(1, 1);
map5.put(6, 6);
map5.put(6, 6);
map5.put(2, 2);
map5.put(5, 5);
System.out.println(map5);// {6=6, 5=5, 3=3, 2=2, 1=1}
}
}
3、ArrayList、LinkedList、Vector
ArrayList和LinkedList是顺序存储结构(基于动态数组的数据结构)和链式存储结构(双向链表的数据结构)的表在java语言中的实现 。
ArrayList提供了一种可增长数组的实现, ArrayList内部使用数组实现,优点是对于get和set操作调用花费常数时间,缺点是插入元素和删除元素会付出昂贵的代价。因为这个操作会导致后面的元素都要发生变动,除非操作发生在集合的末端。
LinkedList是一个双链表的结构,在设计这个这个表结构时,我们需要考虑提供3个类:1、MyLinkedList类本身,2、Node类,该类作为静态的内部类出现,包含一个节点上的数据,以及到前一个节点和后一个节点的链。3、LinkedListIterator类,也是一个私有类,实现Iterator接口,提供next().hannext().remove()方法。
LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
ArrayList 插入,删除数据慢
LinkedList, 插入,删除数据快
ArrayList是顺序结构,所以定位很快,指哪找哪。 就像电影院位置一样,有了电影票,一下就找到位置了。
LinkedList 是链表结构,就像手里的一串佛珠,要找出第99个佛珠,必须得一个一个的数过去,所以定位慢。
Vector 和 ArrayList 一样,都是继承自 AbstractList。它是 Stack 的父类。英文的意思是 “矢量”。
Vector 特点:1、底层由一个可以增长的数组组成。2、Vector 通过 capacity (容量) 和 capacityIncrement (增长数量) 来尽量少的占用空间。3、扩容时默认扩大两倍。4、最好在插入大量元素前增加 vector 容量,那样可以减少重新申请内存的次数。5、通过 iterator 和 lastIterator 获得的迭代器是 fail-fast 的。5、通过 elements 获得的老版迭代器 Enumeration 不是 fail-fast 的。6、同步类,每个方法前都有同步锁 synchronized。
Vector ArrayList
共同点:
都是基于数组
都支持随机访问
默认容量都是 10
都有扩容机制
区别:
Vector 出生的比较早,JDK 1.0 就出生了,ArrayList JDK 1.2 才出来
Vector 比 ArrayList 多一种迭代器 Enumeration
Vector 是线程安全的,ArrayList 不是
Vector 默认扩容 2 倍,ArrayList 是 1.5
都实现了List接口(List接口继承了Collection接口)
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Stack;
public class ListTest
{
public static void main(String[] args)
{
ArrayList<Integer> list1 = new ArrayList<Integer>();
list1.add(3);
list1.add(1);
list1.add(2);
System.out.println(list1);// [3, 1, 2]
LinkedList<Integer> list2 = new LinkedList<Integer>();
list2.add(3);
list2.add(1);
list2.add(2);
System.out.println(list2);// [3, 1, 2]
/*
* Stack继承于Vector,意味着Vector拥有的属性和功能,Stack都拥有
*/
Stack<Integer> stack = new Stack<Integer>();
stack.push(2);// push将元素推入栈中
stack.push(1);
stack.push(3);
System.out.println(stack.peek());// 3 peek()取出栈顶元素,不执行删除
stack.pop();// 取出栈顶元素,并将该元素从栈中删除
System.out.println(stack.peek());// 1 peek()取出栈顶元素,不执行删除
System.out.println(stack.isEmpty());// false 是否为空
}
}
HashCode方法
Java中的集合(Collection)有两类,一类是List,再有一类是Set。前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?
这就是Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。
于是,Java采用了哈希表的原理。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。 所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
Java对于eqauls方法和hashCode方法是这样规定的:
1、如果两个对象相同,那么它们的hashCode值一定要相同;
2、如果两个对象的hashCode相同,它们并不一定相同。
上面说的对象相同指的是用eqauls方法比较。
equals方法主要是用来判断从表面上看或者从内容上看,2个对象是不是相等。
hashcode这个方法是用来鉴定2个对象是否相等的。hashcode方法一般用户不会去调用。我们一般在覆盖equals的同时也要 覆盖hashcode,让他们的逻辑一致。
要从物理上判断2个对象是否相等,用==就可以了。
博客:https://www.cnblogs.com/wl0000-03/p/6019627.html