Java SE基础(十六)集合

  • 集合
  • 集合体系
  • 数据结构简述
    • 队列
    • 数组
    • 链表
  • Collection集合
    • List集合
      • ArrayList集合
      • LinkedList集合
    • Set集合
      • HashSet
      • LinkedHashSet
      • TreeSet
  • Map集合
    • HashMap
  • Collection集合工具类
  • Hash与equals
    • hashcode
    • HashSet元素保证唯一性

之前有写过一篇:ArrayList基础

再补充一些:

集合

集合是用来存储多个同类型数据的容器,不同于数组的定长结构,集合从长度是可变的,使用起来也就更加灵活。

集合体系


可以看出,集合的体系还是很庞大的。。。

集合的顶层都是接口,Collection是单列集合的顶层接口,Map是双列集合的顶层接口。

Collection接口(单列集合)的子体系:
List体系:有序,可重复。
Set:无序,唯一。

数据结构简述

数据结构就是计算机存储、组织数据的方式。

单片机和PLC都有这种方式,FILO(First In Last Out)先入后出。从嵌入式C的角度解释,执行子程序时会压栈,压栈点保存的是调用子程序时的状态,子程序执行完毕后,弹栈,从原先的位置继续执行。

队列

单片机和PLC也有这种方式,FIFO(First In First Out)先入先出。

数组

长度固定,直接分配好内存空间。根据索引计算偏移量来找到对应的数据。数组的查询与修改非常快,但是数组已满时不能增加长度,删除也不快。

链表

长度不固定,每个节点都有数据域和指针域。链表的增加与删除只需要断开其中的某个连接并重新指向和连接即可,故增加与删除很快。由于没有索引,查找与修改慢。

节点只有1个指针域指向下一个节点就是单链。当单链的尾部节点指向头部节点,就会变成环链

单向(非环链)的方式不便于逆序查找数据,将指针域扩充为2个,分别记录上一个节点与下一个节点,就进化成双向链

Collection集合

由于Collection是接口,不能new实例对象,但是可以通过多态的方式向上转型

Collection<String> list = new ArrayList<>();

<String>就是一个泛型,用来限定集合种存储的数据类型。泛型只能是引用数据类型。例如:不能使用int,需要用对应的包装类Integer

如果定义时没写泛型,集合存储的就是Object类型。。。意味着集合可以存储任意类型的数据,降低集合的安全性。。。

public class Run {public static void main(String[] args) {//通过多态的形式,创建Collection集合对象,用来存储字符串Collection<String> collection1= new ArrayList<>();//调用Collection接口的add()方法,添加字符串//public boolean add(E e)用于添加元素,由于返回值始终是true,返回值没什么意义//E 泛型类型 T(Type) 类型 E(Element) 元素 K(Key) 键 V(Value) 值//自定义泛型的时候,可以使用其他字母(因为约定俗成用上述字母,不建议这么做)//在这 E 定义集合的时候 泛型的类型给的是什么 在这里E代表的就是什么//如果定义集合不写泛型 集合存储的就是Object类型 意味着集合可以存储任意的类型 降低集合安全性collection1.add("hello");collection1.add("world");collection1.add("and");collection1.add("java");System.out.println("collection1 = " + collection1);//System.out.println("collection1.remove(\"java\") = " + collection1.remove("java"));//System.out.println("collection1.remove(\"World\") = " + collection1.remove("World"));//ture代表移除元素成功,false代表移除元素失败//collection1.clear();//System.out.println("collection1 = " + collection1);//清除全部数据System.out.println("collection1.contains(\"java\") = " + collection1.contains("java"));System.out.println("collection1.contains(\"World\") = " + collection1.contains("World"));//true代表包含该元素,false代表不包含该元素System.out.println("collection1.isEmpty() = " + collection1.isEmpty());//collection1.clear();System.out.println("collection1.isEmpty() = " + collection1.isEmpty());//isEmpty()用于判断是否为空(是否包含元素,不是判断是否为null)//true代表元素个数为0,false代表元素个数不为0System.out.println("collection1.size() = " + collection1.size());//collection1.clear();//System.out.println("collection1.size() = " + collection1.size());//判断元素个数//通过迭代器遍历集合,Iterator是一个接口Iterator<String> iterator1 = collection1.iterator();while (iterator1.hasNext()){//hasNext用于判断下一个位置是否包含元素String string = iterator1.next();System.out.println("string = " + string);}}
}

运行后:

collection1 = [hello, world, and, java]
collection1.contains("java") = true
collection1.contains("World") = false
collection1.isEmpty() = false
collection1.isEmpty() = false
collection1.size() = 4
string = hello
string = world
string = and
string = javaProcess finished with exit code 0

再来看看:

public class Student {private String name;private int age;@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public Student(String name, int age) {this.name = name;this.age = age;}public Student() {}
}
public class Run {public static void main(String[] args) {//创建集合对象Collection<Student> collection1 = new ArrayList();//创建元素对象Student student1 = new Student("张三",13);Student student2 = new Student("李四",14);Student student3 = new Student("王五",15);//添加元素collection1.add(student1);collection1.add(student2);collection1.add(student3);//直接添加匿名元素对象collection1.add(new Student("马六",16));collection1.add(new Student("赵七",17));collection1.add(new Student("王八",18));//使用迭代器遍历元素//获取迭代器对象//Iterator iterator1 = collection1.iterator();//需要限制类型为Student,否则默认为Object,会导致下方代码报错Iterator<Student> iterator1 = collection1.iterator();while (iterator1.hasNext()){//判断下一个位置是否包含元素System.out.println("iterator1 = " + iterator1);//会不断重复iterator1 = java.util.ArrayList$Itr@1b6d3586////Process finished with exit code -1//∵相当于是一个while(true)的死循环//System.out.println("iterator1.next().getName() = " + iterator1.next().getName());//System.out.println("iterator1.next().getAge() = " + iterator1.next().getAge());//这样做输出结果错位,∵每次调用next()都会使得索引下移一位。。。如果不是倍数关系,容易导致索引越界等异常Student student = iterator1.next();//上方代码不限定类型会导致本行报错//Student student = (Student) iterator1.next();//上方代码不限定类型会导致本行报错,需在本行向下转型System.out.println("student = " + student);}}
}

运行后:

iterator1 = java.util.ArrayList$Itr@135fbaa4
student = Student{name='张三', age=13}
iterator1 = java.util.ArrayList$Itr@135fbaa4
student = Student{name='李四', age=14}
iterator1 = java.util.ArrayList$Itr@135fbaa4
student = Student{name='王五', age=15}
iterator1 = java.util.ArrayList$Itr@135fbaa4
student = Student{name='马六', age=16}
iterator1 = java.util.ArrayList$Itr@135fbaa4
student = Student{name='赵七', age=17}
iterator1 = java.util.ArrayList$Itr@135fbaa4
student = Student{name='王八', age=18}Process finished with exit code 0

创建集合对象时需要注意泛型数据类型一定要限制正确,使用iterator迭代器要使用next方法才能指向下一个,否则会跳不出死循环。但是多次使用next方法会向下重新指向多次,造成数据遗漏。使用时要注意next方法的次数。

List集合

其实学习了父类后,实现类子类大同小异。

public class Run {public static void main(String[] args) {//创建List集合用来存储字符串List<String> list1 = new ArrayList<>();//使用add()方法添加元素list1.add("hello");list1.add("world");list1.add("and");list1.add("java");list1.add("and");list1.add("C#");//插入元素add(int index, E element)//index最大值只能使用到集合的长度,否则会出现索引越界的异常list1.add(5,"and");//public E remove(int index) 根据角标删除元素 返回值是被删除之后的元素String remove1 = list1.remove(1);System.out.println("remove1 = " + remove1);//public E set(int index, E element) 根据索引修改元素 返回值是修改前的元素内容//index最大值只能到最后一个元素String set1 = list1.set(1, "php");System.out.println("set1 = " + set1);System.out.println("--**********************--");//public E get(int index) 通过我们索引值获取元素内容//集合.fori 或者 itar 快速生成遍历for (int i = 0; i < list1.size(); i++) {String string1 = list1.get(i);System.out.println("string1 = " + string1);}System.out.println("--**********************--");//通过迭代器遍历List集合Iterator<String> iterator1 = list1.iterator();//记得限制类型为String,否则默认为Object,后续还需向下转型while (iterator1.hasNext()){System.out.println("iterator1.next() = " + iterator1.next());}System.out.println("--**********************--");}
}

执行后:

remove1 = world
set1 = and
--**********************--
string1 = hello
string1 = php
string1 = java
string1 = and
string1 = and
string1 = C#
--**********************--
iterator1.next() = hello
iterator1.next() = php
iterator1.next() = java
iterator1.next() = and
iterator1.next() = and
iterator1.next() = C#
--**********************--Process finished with exit code 0

再看一段代码:

public class Run {public static void main(String[] args) {//创建自定义对象并遍历List<Student> list1 = new ArrayList<>();//创建及存储元素对象list1.add(new Student("张三",13));list1.add(new Student("李四",14));list1.add(new Student("王五",15));list1.add(new Student("马六",16));//迭代器遍历Iterator<Student> iterator1 = list1.iterator();while (iterator1.hasNext()){Student student1 = iterator1.next();System.out.println("student1 = " + student1);}System.out.println("--********************--");//普通for循环遍历for (int i = 0; i < list1.size(); i++) {Student student1 = list1.get(i);System.out.println("student1 = " + student1);}}
}

成功遍历:

student1 = Student{name='张三', age=13}
student1 = Student{name='李四', age=14}
student1 = Student{name='王五', age=15}
student1 = Student{name='马六', age=16}
--********************--
student1 = Student{name='张三', age=13}
student1 = Student{name='李四', age=14}
student1 = Student{name='王五', age=15}
student1 = Student{name='马六', age=16}Process finished with exit code 0

再来试试正反向遍历:

public class Run {public static void main(String[] args) {//创建List集合,用来存储字符串List<String> list1 = new ArrayList<>();//向List集合添加字符串list1.add("hello");list1.add("world");list1.add("java");//获取列表迭代器ListIterator<String> listIterator1 = list1.listIterator();//先正向遍历while (listIterator1.hasNext()){System.out.println("listIterator1.next() = " + listIterator1.next());}//再反向遍历while (listIterator1.hasPrevious()){String string = listIterator1.previous();System.out.println("string = " + string);}}
}

运行后:

listIterator1.next() = hello
listIterator1.next() = world
listIterator1.next() = java
string = java
string = world
string = helloProcess finished with exit code 0

成功实现了反向遍历。

再来看看并发修改异常:

public class Run {public static void main(String[] args) {//创建List集合,用来存储字符串List<String> list1 = new ArrayList<>();//向List集合添加元素(字符串值)list1.add("hello");list1.add("world");list1.add("java");//并发修改异常出现//判断字符为java时添加元素C#ListIterator<String> listIterator1 = list1.listIterator();
/*         while (listIterator1.hasNext()){String string1 = listIterator1.next();if ("java".equals(string1)){list1.add("C#1");}}
*///使用普通for循环
//        for (int i = 0; i < list1.size(); i++) {//            String string1 = list1.get(i);
//            if ("java".equals(string1)){//                list1.add("C#2");
//            }
//        }//用列表迭代器操作//异常ConcurrentModificationException//和上方for循环共存时报错while (listIterator1.hasNext()){String string2 = listIterator1.next();if ("java".equals(string2)){listIterator1.add("C#3");}}System.out.println("list1 = " + list1);}
}

使用迭代器遍历的同时又使用了list集合本身添加元素会导致并发修改异常

解决方案:使用普通for循环或使用列表迭代器

ArrayList集合

ArrayList集合的底层是数组。

public class Run {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("hello");list.add("world");list.add("java");for (String s : list) {//iter增强for循环,底层是普通迭代器System.out.println("s = " + s);if("world".equals(s)){//list.add("java");}}for (int i = 0; i < list.size(); i++) {System.out.println("list["+i+"] = " + list.get(i));if("world".equals(list.get(i))){list.add("java");list.add("java");}}}
}

使用iter增强for循环,会发现也存在并发修改异常:

Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)at java.util.ArrayList$Itr.next(ArrayList.java:859)at com.ad.day20210402.Demo11.Run.main(Run.java:13)Process finished with exit code 1

这是因为增强for循环的底层是普通迭代器。只好使用普通for循环进行操作。

ArrayList和数组有些差别,ArrayList是使用size方法获取长度,使用get(i)获取内容。

嵌套使用也没问题:

public class Run {public static void main(String[] args) {ArrayList<HashMap<String,String>> list = new ArrayList<>();HashMap<String,String> map1 = new HashMap<>();map1.put("AA","aa");map1.put("BB","bb");HashMap<String,String> map2 = new HashMap<>();map2.put("CC","cc");map2.put("DD","dd");HashMap<String,String> map3 = new HashMap<>();map3.put("EE","ee");map3.put("FF","ff");list.add(map1);list.add(map2);list.add(map3);for (int i = 0; i < list.size(); i++) {HashMap<String,String> maps = list.get(i);//遍历list集合//System.out.println("maps = " + maps);Set<String> keys = maps.keySet();for (String key : keys) {//增强for循环遍历map集合的键值对System.out.println("maps.get(key) = " + maps.get(key));}}System.out.println("--************--");//        Set<Map.Entry<String,String>> entries = maps.entrySet();
//        for (Map.Entry<String, String> entry : entries) {//            String key = entry.getKey();
//
//        }}
}

运行后:

maps.get(key) = aa
maps.get(key) = bb
maps.get(key) = cc
maps.get(key) = dd
maps.get(key) = ee
maps.get(key) = ff
--************--Process finished with exit code 0

集合很灵活。

LinkedList集合

LinkedList集合的底层是链表。

public class Run {public static void main(String[] args) {LinkedList<String> list = new LinkedList<>();list.add("java");list.add("hello");list.add("world");for (int i = 0; i < list.size(); i++) {System.out.println(list.get(i));}System.out.println("--------------");list.addFirst("android");list.addLast("php");System.out.println("--------------");list.removeFirst();list.removeLast();System.out.println("---------------");String first = list.getFirst();System.out.println("first = " + first);String last = list.getLast();System.out.println("last = " + last);System.out.println(list);}
}

基本一致,多了getFirst与getLast、addFirst与addLast、removeFirst与removeLast等涉及操作头尾的方法:

java
hello
world
--------------
--------------
---------------
first = java
last = world
[java, hello, world]Process finished with exit code 0

关于List及其子类不再赘述。

Set集合

Set集合是Collection集合的子类,元素特点是无序,唯一。

HashSet

底层数据结构是哈希表,这种结构增删改查都很快(Hashtable是建立数组,各数据对数组长度取模后获得索引,数组内存储的是单链的引用,每个数据必然有一个索引号,改查很快。同时,增删是对单链操作,也很快)。

元素的存取顺序可能不一致。由于没有索引,不能用普通for循环遍历。由于继承自set集合,不能重复。

看个简单的使用:

public class Run {public static void main(String[] args) {Set<String> set = new HashSet<>();set.add("hello");set.add("world");set.add("java");set.add("world");Iterator iterator = set.iterator();while (iterator.hasNext()){System.out.println("iterator.next() = " + iterator.next());}}
}

运行后:

iterator.next() = world
iterator.next() = java
iterator.next() = helloProcess finished with exit code 0

同样可以使用iterator迭代器遍历,同样有next方法指向下一个元素。

如果改成这样:

public class Run {public static void main(String[] args) {HashSet<String> hs = new HashSet<>();hs.add("hello");hs.add("world");hs.add("java");hs.add("world");hs.add(null);hs.add(null);//itar快捷键for (String i: hs) {System.out.println("hs=" + i);}}
}

运行后:

hs=null
hs=world
hs=java
hs=helloProcess finished with exit code 0

显然,HashSet集合的元素不能重复,且读取顺序与存储顺序不一致。

LinkedHashSet

底层的数据结构是哈希表+链表,故元素的存取顺序是一致的,也没有重复的元素。

public class Run {public static void main(String[] args) {LinkedHashSet<String> lhs = new LinkedHashSet<>();lhs.add("hello");lhs.add("null");lhs.add("world");lhs.add("java");lhs.add("null");lhs.add("java");for (String lh : lhs) {System.out.println("lh = " + lh);}}
}

运行后:

lh = hello
lh = null
lh = world
lh = javaProcess finished with exit code 0

显然,LinkedHashSet集合的元素不能重复,且读取顺序与存储顺序一致。

TreeSet

public class Run {public static void main(String[] args) {TreeSet<Integer> ts = new TreeSet<>();ts.add(10);ts.add(12);ts.add(12);ts.add(21);ts.add(13);System.out.println("ts = " + ts);//ts = [10, 12, 13, 21]自动排序}
}

执行后:

ts = [10, 12, 13, 21]Process finished with exit code 0

可以看出,TreeSet可以自动按照从小到大排序,且不能有重复数据。

Map集合

Map集合是双列集合的顶层接口,用来存储键值对对象,键是唯一的,值可以重复。

Map类的子类使用keySet方法获取集合中的所有键并返回Set类型的keys,然后用iter(增强for循环)遍历keys,使用get(key)可以获取对应的value。

也可以使用entrySet方法获取所有键值对对象的集合entrys,再用增强for循环,使用Map对象.entry获得单个键值对,再用getKey和getValue方法获取对应的键及值。

HashMap

public class Run {public static void main(String[] args) {Map<String,String> hashmap = new HashMap<>();hashmap.put("a001","aaa");hashmap.put("a001","bbb");//键不能重复,之前有这个键就会重复(值覆盖之前的值),之前没有这个键就添加hashmap.put("a002","aba");hashmap.put("a004","baa");hashmap.put("a003","abb");hashmap.put("a005","abb");System.out.println("hashmap = " + hashmap);//hashmap = {a005=abb, a001=aaa, a002=aba, a003=abb, a004=baa}//说明key是无序的且不可重复的}
}

运行后:

hashmap = {a005=abb, a001=bbb, a002=aba, a003=abb, a004=baa}Process finished with exit code 0

可以看出HashMap的键值对是无序存放的,且键不能重复,如果重复,后一个value会覆盖前一个value。

再来测试下HashMap的方法:

public class Run {public static void main(String[] args) {Map<String,String> map = new HashMap<>();map.put("王宝强","马蓉");map.put("贾乃亮","李小璐");map.put("陈羽凡","白百何");map.put("谢霆锋","张柏芝");System.out.println("map.containsKey(\"王宝强\") = " + map.containsKey("王宝强"));System.out.println("map.containsValue(\"王宝强\") = " + map.containsValue("王宝强"));System.out.println("map = " + map);String 谢霆锋 = map.remove("谢霆锋");System.out.println("map = " + map);System.out.println("map.size() = " + map.size());for (Map.Entry<String, String> stringStringEntry : map.entrySet()) {System.out.println("stringStringEntry.getKey() = " + stringStringEntry.getKey());System.out.println("stringStringEntry.getValue() = " + stringStringEntry.getValue());}map.clear();System.out.println("map.isEmpty() = " + map.isEmpty());System.out.println("map = " + map);}
}

执行后:

map.containsKey("王宝强") = true
map.containsValue("王宝强") = false
map = {贾乃亮=李小璐, 王宝强=马蓉, 谢霆锋=张柏芝, 陈羽凡=白百何}
map = {贾乃亮=李小璐, 王宝强=马蓉, 陈羽凡=白百何}
map.size() = 3
stringStringEntry.getKey() = 贾乃亮
stringStringEntry.getValue() = 李小璐
stringStringEntry.getKey() = 王宝强
stringStringEntry.getValue() = 马蓉
stringStringEntry.getKey() = 陈羽凡
stringStringEntry.getValue() = 白百何
map.isEmpty() = true
map = {}Process finished with exit code 0

使用Map.keySet还要更少一步:

Set<String> keys = map.keySet();for (String key : keys) {String value = map.get(key);System.out.println(key + "=" + value);}

HashMap的键值对都可以是引用数据类型,故键值对不仅可以是String,还可以是ArrayList<String>这种集合:

public class Run {public static void main(String[] args) {HashMap<String, ArrayList<String>> maps = new HashMap<>();//ctrl+art+v可以自动生成 art+enterArrayList<String> sanguoyanyi = new ArrayList<>();sanguoyanyi.add("诸葛亮");sanguoyanyi.add("赵云");ArrayList<String> xiyouji = new ArrayList<>();xiyouji.add("猴子");xiyouji.add("猪哥");ArrayList<String> shuihuzhuan = new ArrayList<>();shuihuzhuan.add("武松");shuihuzhuan.add("吴用");maps.put("三国演义",sanguoyanyi);maps.put("西游记",xiyouji);maps.put("水浒传",shuihuzhuan);Set<String> keys = maps.keySet();for (String key : keys) {System.out.print(key + ":");ArrayList<String> values = maps.get(key);for (String value : values) {System.out.print(value + ",");}System.out.println();}}
}

运行后:

水浒传:武松,吴用,
三国演义:诸葛亮,赵云,
西游记:猴子,猪哥,Process finished with exit code 0

说明集合是可以嵌套的。

Collection集合工具类

针对集合操作的工具类。

public class Run {public static void main(String[] args) {ArrayList<Integer> list = new ArrayList<>();//ctrl + art+ vlist.add(10);list.add(11);list.add(2);list.add(20);list.add(25);System.out.println("排序前list = " + list);Collections.sort(list);//正向排序System.out.println("排序后list = " + list);Collections.reverse(list);System.out.println("反转后list = " + list);for (int i = 0; i < 20; i++) {Collections.shuffle(list);//随机打乱集合中元素顺序,洗牌System.out.println("洗牌后list"+i+"\t"+" = " + list);//每次的结果都不同}}
}

执行后:

排序前list = [10, 11, 2, 20, 25]
排序后list = [2, 10, 11, 20, 25]
反转后list = [25, 20, 11, 10, 2]
洗牌后list0     = [25, 10, 20, 2, 11]
洗牌后list1     = [10, 2, 20, 25, 11]
洗牌后list2     = [10, 25, 20, 2, 11]
洗牌后list3     = [25, 20, 10, 11, 2]
洗牌后list4     = [11, 10, 2, 25, 20]
洗牌后list5     = [20, 2, 25, 10, 11]
洗牌后list6     = [25, 10, 11, 2, 20]
洗牌后list7     = [11, 10, 25, 20, 2]
洗牌后list8     = [10, 2, 25, 20, 11]
洗牌后list9     = [10, 20, 25, 11, 2]
洗牌后list10    = [10, 2, 11, 25, 20]
洗牌后list11    = [11, 10, 20, 2, 25]
洗牌后list12    = [10, 2, 20, 11, 25]
洗牌后list13    = [10, 20, 11, 2, 25]
洗牌后list14    = [2, 10, 20, 25, 11]
洗牌后list15    = [2, 10, 25, 20, 11]
洗牌后list16    = [2, 20, 11, 10, 25]
洗牌后list17    = [11, 25, 20, 10, 2]
洗牌后list18    = [25, 11, 20, 10, 2]
洗牌后list19    = [11, 20, 25, 10, 2]Process finished with exit code 0

Hash与equals

hashcode

public class Student {private String name;private int age;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public Student(String name, int age) {this.name = name;this.age = age;}public Student() {}//    @Override
//    public boolean equals(Object o) {//        if (this == o) return true;
//        if (o == null || getClass() != o.getClass()) return false;
//        Student student = (Student) o;
//        return age == student.age &&
//                name.equals(student.name);
//    }
//
//    @Override
//    public int hashCode() {//        return Objects.hash(name, age);
//    }
}
public class Run {public static void main(String[] args) {System.out.println("你好".hashCode());//输出字符串的哈希值652829System.out.println("通话".hashCode());//1179395System.out.println("重地".hashCode());//1179395//hashCode不同内容一定不同(只能判断是否不同),// hashCode相同内容不一定相同(不能判断内容是否相同,此时需要重写equals方法)Student stu1 = new Student("张三", 13);Student stu2 = new Student("张三", 13);System.out.println("stu1.hashCode() = " + stu1.hashCode());//stu1.hashCode() = 325040804System.out.println("stu2.hashCode() = " + stu2.hashCode());//stu2.hashCode() = 1173230247//发现内容一致的2个实例对象的hashcode值不同。。。需要改写//改写后//stu1.hashCode() = 24022533//stu2.hashCode() = 24022533}
}


idea可以很方便地重写hashcode与equals方法。

从本例可以看出,hashcode只能用于判断不同,不能用于判断相同(“重地”与“通话”具有相同的hashcode)。

HashSet元素保证唯一性

进入HashSet.java观看源码,发现:

public HashSet() {map = new HashMap<>();}

原来HashSet的底层是HashMap。。。


HashMap.java中发现:

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}

这货就是添加数据用的方法,再次定位:

发现这样的代码:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

上方的615~624行:

    /*** Implements Map.put and related methods.** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/

解释了这些参数的含义。

节点为null时的重定义哈希表长度和新建节点之类的初始化操作不是重点,已经有数据以后的逻辑才需要关注。于是可以聚焦于:

if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;

这一句是在判断哈希表之前是否已经有完全相同的元素,有&&||,就涉及到短路。如果传入的键的hash值与表中键的hash值不同,说明不是一个值,直接跳出。

hash值相同仍然存在是同一个值的可能性,继续判断&&右侧()的内容:
key相等说明key是同一个内存地址的key,必然相同,跳过右边直接e=p;

如果传入的不是同一个key,才会去判断key是否不为空且与原来的key(p.key)完全一致,不为空且内容完全一致才会e=p;

于是,当传入相同的key时,新节点e指向原节点p。在后续步骤中,会覆盖这个key对应的值,键值对存储中只改变值不改变键,于是保证了键的唯一性(值可以重复)。


回过头看HashSet.java

public boolean add(E e) {return map.put(e, PRESENT)==null;}

显然是把元素作为键传入了HashMap作为键,此时HashMap的值PRESENT是什么无关痛痒。进去看看也无妨:

private static final Object PRESENT = new Object();

是一个被final修饰的常量,Object类。

由此可知:

HashSet的底层是HashMap(存储键值对,键是唯一的,值每次都new一个Object类的实例对象)。

不得不佩服,编写JVM的前辈还是很严谨的。equals方法100%可靠但效率低下,hashcode方法效率高但不准确,为了尽量减少equals方法的使用频率,前辈也是操碎了心。

我们也应该参考这种做法,判断集合中对象是否相同时,依次判断hashcode、指向的地址值、是否为null、各个属性是否相同。这种方式很高效,idea的重写equals方法就是这样实现的!!!idea真是个很好的ide。

Java SE基础(十六)集合相关推荐

  1. Java必备基础十六——输入与输出流相关类详细介绍

    坚持的第16篇. 鸡汤一下:真正能让你走远的,都是自律.积极和勤奋 文章目录 序言 一.什么是流 二.流的种类划分 1.按数据流的方向分:输入流.输出流 1.1 定义 1.2 层次结构 2.按处理数据 ...

  2. java se系列(十二)集合

    1.集合 1.1.什么是集合 存储对象的容器,面向对象语言对事物的体现,都是以对象的形式来体现的,所以为了方便对多个对象的操作,存储对象,集合是存储对象最常用的一种方式.集合的出现就是为了持有对象.集 ...

  3. Java SE 第十六讲----面向对象特征之继承

    1.继承(inheritance):Java是单继承的,意味着一个类只能从另一个类继承(被继承的类叫做父类也叫[基类 baseclass]),继承的类叫做子类,java中的继承使用extends关键字 ...

  4. Java SE 基础(十)Java中的异常

    Java SE 基础(十)Java中的异常 什么是异常 异常的处理 异常类 throw和throws 自定义异常 什么是异常 Java 中处理错误的一种机制,Java 中的错误大致分为两类,一类是编译 ...

  5. Java SE基础(十四)常用API

    Java SE基础(十四)常用API 概述 Object类 构造方法 成员方法 toString()方法 equals(Object obj)方法 Arrays排序 Arrays类概述 Arrays成 ...

  6. JAVA SE基础知识总结

    JAVA基础篇 1_JAVA语言概述 1.1 JAVA的总体概述 1.2 JAVA语言概述 1.2.1 基础常识 1.2.2 计算机语言的发展迭代史 1.2.3 Java语言版本迭代概述 1.2.4 ...

  7. 面试必会系列 - 1.1 Java SE 基础

    本文已收录至 github,完整图文:https://github.com/HanquanHq/MD-Notes Java SE 基础 面向对象 Java 按值调用还是引用调用? 按值调用指方法接收调 ...

  8. Java复习总结(二)Java SE基础知识

    Java SE面试题 目录 Java SE基础 基本语法 数据类型 关键字 面向对象 集合 集合类概述 Collection接口 进阶 线程 锁 JDK 反射 JVM GC io操作和NIO,AIO ...

  9. Java SE 基础知识

    Java SE 基础知识 1 2 @(Notes)[J2SE, Notes] VICTORY LOVES PREPARATION. 特别说明: 该文档在马克飞象查阅最佳: 本部分知识还在迭代中,欢迎补 ...

最新文章

  1. 《研磨设计模式》chap24 桥接模式bridge(1)基本概念
  2. Scala类的继承和抽象类
  3. linux__ftp
  4. AEM中的单元测试(大声思考)
  5. 注会考试不可以用计算机,CPA机考,不让带计算器该如何做?
  6. Android视频开发基础(二)
  7. JDK9.0.4环境变量配置
  8. [Java] 蓝桥杯ADV-202 算法提高 最长公共子序列
  9. 软件设计之UML的几种关系
  10. Git基础教程(一)
  11. mysql中的编号怎么自动加_MySQL中实现ID编号自动增加的方法
  12. suse12 sp4,sp5镜像资源分享
  13. python统计形容词权重然后排序
  14. 2021最新前端面试题
  15. 计算机系统中文件命名的,你电脑上的文件命名规范吗
  16. OXY OPENCART 商城自适应主题模板 ABC-0020-01
  17. 用于app的支付成功与失败页面
  18. 新书上市第13天,在亚马逊Kindle电子书人工智能榜第三,与《未来简史》和李开复《人工智能》同榜
  19. 计算机视觉检测外观,大米外观品质计算机视觉检测的研究
  20. 大小写英文字母对应的ASCII值

热门文章

  1. kernel panic
  2. 在线绘制图表工具的使用
  3. 计算机控制键盘,键盘装置及其计算机控制系统的制作方法
  4. C++中用 GetModuleFileName()函数 获得程序当前的运行目录
  5. AES - Openssl AES 函数说明
  6. Linux环境变量profile
  7. 推荐系统论文阅读——Factorizing Personalized Markov Chains for Next-Basket Recommendation
  8. 网络安全技术 3.28 作业
  9. 守护进程 - Supervisor的使用
  10. html 中精灵图使用