HashSet直接继承自Set接口;TreeSet是继承自SortedSet接口,SortedSet继承自Set接口。
HashSet
特征
(1)存储时的顺序和取出来的顺序不同(无序指的是没有下标)
(2)不可重复;
(3)放到HashSet集合中的元素实际上是放到HashMap集合的key上边了。
例:
public class A{
public static void main(String[] args) {
Set<String> hs = new HashSet<>();
hs.add("a");
hs.add("1");
hs.add("10");
hs.add("3");
hs.add("b");
hs.add("g");
System.out.println(hs); // [a, 1, b, 3, g, 10]
for (String item : hs) {
System.out.println(item);
}
}
}
TreeSet
特征
(1)无序,不可重复的,存储的元素会自动按照大小进行排序,称为可排序集合;
public class A{
public static void main(String[] args) {
Set<String> hs = new TreeSet<>();
hs.add("a");
hs.add("z");
hs.add("h");
hs.add("k");
hs.add("y");
hs.add("b");
System.out.println(hs); // [a, b, h, k, y, z]
for (String item : hs) {
System.out.println(item);
}
}
}
Map<K,V>
HashSet和TreeSet底层都是Map,下边记录下Map接口。
特点
(1)以键值对(key、value)形式存储数据,key和value都是引用类型,都是存储对象引用,key起主导作用,value只是key的附属品。
(2)Map和Collection没有继承关系。
(3)key特点:无序(为什么无序下边put原理可以明白)、不可重复(不可重复原因equals方法作为保证,详情查看下边put原理)。
Map接口常用方法
(1)void clear( )
清空Map集合。
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
map.clear(); // 清空集合
System.out.println(map.size()); // 0
}
}
(2)boolean containsKey( Object key )
判断Map集合中是否包含某个key。
containsKey() 方法内部会调用equals() 来判断是否相等
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
System.out.println(map.containsKey("name")); // true
System.out.println(map.containsKey(new String("name"))); // true
}
}
(3)boolean containsValue( Object value )
判断Map集合中是否包含某个value。
containsValue() 方法内部会调用equals() 来判断是否相等
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
System.out.println(map.containsValue("20")); // true
System.out.println(map.containsValue(new String("20"))); // true
}
}
(4)V get( Object key )
向Map集合中添加键值对。
get() 方法获取一个不存在的key返回null
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
System.out.println(map.get("address")); // "shandong"
System.out.println(map.get(new String("0"))); // null
}
}
(5)boolean isEmpty()
判断Map集合中元素个数是否为0。
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
System.out.println(map.isEmpty()); // false
}
}
(6)Set<K> keySet()
获取Map集合中的所有key(所有的键是一个set集合)。
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
Set<String> k = map.keySet();
System.out.println(k); // ["address", "name", "age"]
}
}
(7)Set<Map.Entry<K, V>> entrySet()
Map.Entry<K, V> 就是一个静态内部类,Set集合中存储的类型是Map.Entry类型。
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
// 把Map集合转Set集合,里边的元素为Map.Entry类型
Set<Map.Entry<String, String>> set = map.entrySet();
// 方法一(效率较高,因为会直接从Node对象中获取key和value):
for (Map.Entry<String, String> item : set) {
System.out.println(item.getKey()+"="+item.getValue());
}
// 方法二:
// 遍历Set集合,每次取出来的是一个Node对象(在源码中Node是一个类)
Iterator<Map.Entry<String, String>> it = set.iterator();
while (it.hasNext()) {
Map.Entry<String, String> res = it.next();
String k = res.getKey();
String v = res.getValue();
System.out.println(k+"="+v);
}
}
}
(8)V put(K key, V value)
向Map集合中添加键值对。
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
System.out.println(map.size()); // 3
}
}
(9)V remove(Object key)
通过key删除键值对,返回的是被删除的value。
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
String v = map.remove(new String("name"));
System.out.println(v); // "a"
System.out.println(map); // {address=shandong, age=20}
}
}
(10)int size()
获取Map集合中键值对的个数。
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
System.out.println(map.size()); // 3
}
}
(11)Collection<V> values()
获取Map集合中键值对的个数。
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
Collection<String> vs = map.values();
System.out.println(vs); // ["shandong", "a", "20"]
}
}
遍历Map集合常用方法
(1)通过keySet获取所有的key,然后遍历key获取value值(下边方式遍历效率较低,通常我们会采用:entrySet( ) 方法 )
public class A{
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "a");
map.put("age", "20");
map.put("address", "shandong");
Set<String> k = map.keySet();
// 方法一:
for (String item : k) {
System.out.println(map.get(item));
}
// 方法二:
Iterator<String> it = k.iterator();
while (it.hasNext()) {
String res = it.next();
System.out.println(res);
}
}
}
Hash表的数据结构(散列表)
1、哈希表的数据结构:
它是一个数组和单向链表的结合体。
数组:在查询方面效率较高,随机增删方面效率较低。
单链表:在随机增删方面效率较高,在查询检索方面效率较低。
哈希表将以上的两种数据结构融合在一起,充分发挥了它们的优点。
HashMap源码:
底层实际就是一个数组,
静态内部类HashMap.Node
map.put(k, v)、map.get(k)实现原理:
put() 方法实现原理:
get() 方法实现原理:
为什么哈希表的随机增删,以及查询效率都很高?
增删是在单向链表上完成的,而查询也不需要都扫描,只需要部分扫描。
总结
(1)通过上边示意图,HashMap集合中的key,会先后调用两个方法,一个方法是hashCode,一个方法是equals,且两个方法都需要重写。
(2)同一个单向列表上,所有节点的 hash 值 相同,因为它们的数组下标是一样的,但是,同一链表上k与k的equals方法返回的肯定是false了。
(3)哈希表HashMap使用不当时,无法发挥性能!
假设将所有的hashCode()方法返回的值固定为某个值,那么会导致底层哈希表变为一个纯单向链表啦。这种情况我们称为:散列分布不均匀。
什么是散列分布均匀:
假设有100个元素,10个单向链表,那么每一个单向链表上有10个节点,这是最好的:是散列分布均匀。
假设所有的hashCode()方法返回值都不一样,会有什么问题?
这样会导致底层哈希表称为一个纯一维数组了,没有链表的概念了,也是散列分布不均匀。
(4)HashMap集合的默认初始化容量为16,默认加载因子为:0.75。
默认加载因子:
当HashMap底层数组的容量超出75%的时候,此时数组会自动扩容。
(5)HashMap集合的初始化容量为16,如果你觉着不够用,那么自己定义的容量必须为2的倍数,这也是官方推荐的,这是因为达到散列均匀,为了提高HashMap集合的存储效率,所以必须是2的倍数。
(6)如果一个类的equals() 方法重写了,那么hashCode() 方法必须要重写(原因如下),equals()方法返回true,那么hashCode()返回值必须一样,因为equals() 方法返回true,表示两个对象相同,在同一个单链表上比较的,对于同一个单链表上的节点来说,他们的哈希值都是相同的,所以hashCode() 方法的返回值也应该相同。
下边Student类中,只重写了equals方法,分别把stu和stu1添加到HashSet集合中,按道理元素个数应该是1,因为Studente的quals方法重写了,但是结果却返回2,原因:在new HashSet时,底层其实是new HashMap,等同于向HashMap中添加元素,而向HashMap添加元素,第一步会调用hashCode方法返回哈希值,但是Student类没有重写hashCode方法,所以返回的哈希值不一样,此时会Student类会被放到不同的一个单向链表中,所以size为2,解决此问题也很简单,还需要重写hashCode方法。
public class Person {
public static void main(String[] args) {
Student stu = new Student("lxc");
Student stu1 = new Student("lxc");
Set<Student> students = new HashSet<>();
students.add(stu);
students.add(stu1);
System.out.println(students.size()); // 2
}
}
class Student {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(name, student.name);
}
}
解决:
public class Person {
public static void main(String[] args) {
Student stu = new Student("lxc");
Student stu1 = new Student("lxc");
Set<Student> students = new HashSet<>();
students.add(stu);
students.add(stu1);
System.out.println(students.size()); // 1
}
}
class Student {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
(7)结论:
放在HashMap 的key部分的,以及放在HashSet集合中的元素,都需要重写hashCode和equals方法。
(8)在JDK8中,如果哈希表中单向链表上的元素超过8个,单向链表这种数据结构会变为红黑树数据结构,当红黑树上的节点小于6时,会重新把红黑树变为单向链表,这个改进为性能考虑。
HashMap线程不安全
多线程并发会造成之前put进去的值,get时不是之前的值。
解决方案:
* 方案一:Collections.synchronizedMap(new HashMap<>());
* 方案二:new ConcurrentHashMap();
实际开发建议用 ConcurrentHashMap