Java集合框架全解析:从LinkedHashMap到TreeMap与HashSet面试题实战
一、LinkedHashMap
①LinkedHashMap集合和HashMap集合的用法完全相同。
②不过LinkedHashMap可以保证插入顺序。
③LinkedHashMap集合因为可以保证插入顺序,因此效率比HashMap低一些。
④LinkedHashMap是如何保证插入顺序的?
底层采用了双向链表来记录顺序。
⑤LinkedHashMap集合底层采用的数据结构是:哈希表 + 双向链表。
⑥LinkedHashMap集合的key是:有序不可重复。key部分也需要同时重写hashCode + equals。
⑦key的取值可以为null,key如果相同,value也是覆盖。
二、Hashtable
①Hashtable和HashMap一样,底层也是哈希表。
②Hashtable是线程安全的,方法上都有synchronized关键字。使用较少,因为保证线程安全有其他方式。
③Hashtable的初始化容量:11。默认加载因子:0.75
④Hashtable的扩容策略:2倍。
1. HashMap的key和value都是可以是null。但是Hashtable的key和value都不能为null。
import java.util.HashMap;
import java.util.Map;public class Hashtable {public static void main(String[] args) {Map<Integer, String> map = new HashMap<>();map.put(null, null);System.out.println(map.size());Map<Integer,String> map2 = new Hashtable<>();map2.put(null, "zhangsan");//报错map2.put(1, null);//报错}
}
2.Hashtable中有一些传统方法,这些方法不属于集合框架:
Enumeration keys(); 获取所有key的迭代器
Enumeration elements(); 获取所有value的迭代器
Enumeration的相关方法
boolean hasMoreElements(); 是否含有元素
E nextElement(); 获取元素
public class Hashtable {public static void main(String[] args) {// 在Hashtable中仍然保留着一些比较传统的方法,例如Hashtable中独有的迭代方式。// Hashtable独有的传统的方法,就需要使用Hashtable来调用。java.util.Hashtable<Integer, String> hashtable = new java.util.Hashtable<>();hashtable.put(1, "zhangsan");hashtable.put(2, "lisi");hashtable.put(3, "wangwu");hashtable.put(4, "zhaoliu");// 迭代// 获取含有所有key的迭代器Enumeration<Integer> keys = hashtable.keys();while (keys.hasMoreElements()) {//判断是否有元素Integer key = keys.nextElement();//有元素则返回,且光标下移System.out.println(key);}// 获取含有所有value的迭代器Enumeration<String> values = hashtable.elements();while (values.hasMoreElements()) {String value = values.nextElement();System.out.println(value);}}
}
运行结果:
Hashtable和HashMap集合的区别:
HashMap集合线程不安全,效率高,key和value允许null。
Hashtable集合线程安全,效率低,key和value不允许null。
三、Properties
1.Properties被称为属性类。通常和xxx.properties属性文件一起使用。
2.Properties的父类是Hashtable。因此Properties也是线程安全的。
3.Properties不支持泛型,key和value只能是String类型。
4.Properties相关方法:
Object setProperty(String key, String value); 和put方法一样。
String getProperty(String key); 通过key获取value
Set<String> propertyNames(); 获取所有的key
/*** 1.这里先作为了解。因为后面再IO流当中还是需要使用Properties的,到那个时候就理解了。* 2.java.util.Properties,我们一般叫做:属性类。* 3.Properties继承Hashtable,所以Properties也是线程安全的。Properties也是一个Map集合。* 4.Properties属性类一般和java程序中的属性配置文件联合使用,属性配置文件的扩展名是:xxxxxxx.properties* 5.Properties类不支持泛型。key和value是固定类型,都是String类型。* 6.目前需要掌握的Properties三个方法:* String value = pro.getProperty("name");* pro.setProperty("name", "value");* Enumeration names = pro.propertyNames();*/
public class oop2 {public static void main(String[] args) {// 创建一个属性类对象Properties pro = new Properties();// 往属性类对象中存储key和value,类似于map.put(k, v)pro.setProperty("jdbc.driver", "com.mysql.jdbc.Driver");pro.setProperty("jdbc.user", "root");pro.setProperty("jdbc.password", "123123");pro.setProperty("jdbc.url", "jdbc:mysql://localhost:3306/powernode");// 通过key获取valueString driver = pro.getProperty("jdbc.driver");String user = pro.getProperty("jdbc.user");String password = pro.getProperty("jdbc.password");String url = pro.getProperty("jdbc.url");System.out.println(driver);System.out.println(user);System.out.println(password);System.out.println(url);// 获取所有的keyEnumeration<?> names = pro.propertyNames();while (names.hasMoreElements()) {String name = (String)names.nextElement();String value = pro.getProperty(name);System.out.println(name + "=" + value);}}}
运行结果:
四、二叉树与红黑二叉树
1.二叉树
二叉树(BinaryTree)由一个结点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。
下图中展现了五种不同基本形态的二叉树。
(a) 为空树。
(b) 为仅有一个结点的二叉树。
(c) 是仅有左子树而右子树为空的二叉树。
(d) 是仅有右子树而左子树为空的二叉树。
(e) 是左、右子树均非空的二叉树。
2.排序二叉树
排序二叉树采用左小右大原则存储,按照中序遍历方式,自动就是排好序的
中序遍历:左根右
前序遍历:根左右
后序遍历:左右根
比如:我们要将数据【14, 12, 23, 4, 16, 13, 8, 3】存储到排序二叉树中,如右图所示
排序二叉树的问题:排序二叉树本身实现了排序功能,可以快速检索。但如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成普通的链表,其检索效率就会很差。(失去平衡,排序二叉树就没有意义了)
先进行排序变成:【3, 4, 8, 12, 13, 14, 16, 23】,然后存储到排序二叉树中,显然就变成了链表,如下图所示
3.平衡二叉树(AVL)
为了避免出现上述一边倒的存储,科学家提出了“平衡二叉树”。
在平衡二叉树中任何结点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。 增加和删除结点可能需要通过一次或多次树旋转来重新平衡这个树。
结点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子-2或2的节点被认为是不平衡的,并需要重新平衡这个树。
比如,我们存储排好序的数据【3, 4, 8, 12, 13, 14, 16, 23】,增加结点如果出现不平衡,则通过节点的左旋或右旋,重新平衡树结构,最终平衡二叉树如下图所示
(另参见:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html)
4.红黑二叉树
① 红黑二叉树(简称:红黑树),它首先是一棵二叉树,同时也是一棵自平衡的排序二叉树。
②红黑树在原有的排序二叉树增加了如下几个要求:
1. 每个结点要么红色,要么黑色。
2. 根结点永远是黑色。
3. 所有的叶子结点都是空结点(即null),并且是黑色的。
4. 每个红色结点的两个子结点都是黑色 (从每个叶子结点到根结点的路径上不会有两个连续的红色结点) 。
5. 从任一结点到其子树中每个叶子结点的路径都包含相同数量的黑色结点。
6. 每次新结点在插入时,颜色是红色的。插入后,会根据红黑树的约束条件进行:树的旋转和颜色的调整。
③这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。这样就让树大致上是平衡的。
④红黑树是一个更高效的检索二叉树,JDK 提供的集合类 TreeMap、TreeSet 本身就是一个红黑树的实现。红黑树的基本操作:插入、删除、左旋、右旋、着色。每插入或者删除一个节点,可能会导致树不在符合红黑树的特征,需要进行修复,进行 “左旋、右旋、着色” 操作,使树继续保持红黑树的特性。
五、TreeMap
①TreeMap底层就是红黑树。
②TreeMap和HashMap用法一样,只不过需要key排序的时候,就可以使用TreeMap。
③TreeMap的key不能是null。
④让TreeMap集合的key可排序,有两种方式:
第一种方式:key实现了Comparable接口,并且提供了compareTo方法,在该方法中添加了比较规则。(比较规则不变的话建议这种。)
第二种方式:创建TreeMap集合时,传一个比较器,比较器实现Comparator接口,在compare方法中添加比较规则。
1.测试TreeMap的key是可排序的
/*** java.util.TreeMap* 1. TreeMap集合的key部分是可排序的。(但不可重复。)* 2. TreeMap集合的key也需要重写hashCode + equals。* 3. TreeMap集合底层采用了红黑二叉树。*/
public class TreeMapTest01 {public static void main(String[] args) {// 创建TreeMap集合TreeMap<Integer, String> map = new TreeMap<>();// 存放map.put(100, "zhangsan");map.put(101, "李四");map.put(102, "wangwu");map.put(99, "赵六");map.put(88, "qianqi");// 遍历Set<Map.Entry<Integer, String>> entries = map.entrySet();for(Map.Entry<Integer, String> entry: entries){System.out.println(entry.getKey() + "=" + entry.getValue());}}
}
运行结果:
2.实现Comparable接口提供比较规则
* 如果key是自定义类型的,能排序吗? *
默认情况下是不行的,会出现:ClassCastException *
底层会将key向下转型为:Comparable接口类型。
Person类:
import java.util.Objects;public class Person {private String name;private int age;public Person() {}public Person(String name, int age) {this.name = name;this.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;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return Objects.equals(name, person.name);}@Overridepublic int hashCode() {return Objects.hash(name);}
}
测试类:
/*** 如果key是自定义类型的,能排序吗?* 默认情况下是不行的,会出现:ClassCastException* 底层会将key向下转型为:Comparable接口类型。*/
public class TreeMapTest02 {public static void main(String[] args) {// 创建Map集合Map<Person, String> persons = new TreeMap<>();// 创建PersonPerson p1 = new Person("bbc", 20);Person p2 = new Person("abc", 19);Person p3 = new Person("bbb", 5);Person p4 = new Person("ccc", 25);Person p5 = new Person("aaa", 25);// 添加// java.lang.ClassCastException// class Person cannot be cast to class java.lang.Comparablepersons.put(p1, "1");persons.put(p2, "2");persons.put(p3, "3");persons.put(p4, "4");persons.put(p5, "5");System.out.println(persons);}
}
运行结果:
* 这种排序方式是让key元素实现Comparable接口。
* 这种设计方案有点侵入式。
*
* 什么时候使用这种方式?比较规则不会发生改变的时候。
⑴.key实现了Comparable接口,并且提供了compareTo方法,在该方法中添加了比较规则
Person类:
public class Person implements Comparable<Person> {private String name;private int age;public Person() {}public Person(String name, int age) {this.name = name;this.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;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return Objects.equals(name, person.name);}@Overridepublic int hashCode() {return Objects.hash(name);}@Overridepublic int compareTo(Person o) {// 编写比较规则// 按照年龄排序//return this.age - o.age; // this在前是升序。o在前是降序。// 按照名字排序//return this.name.compareTo(o.name);// 先按照名字进行排序,如果名字相同,则按照年龄排序。/*if(this.name.equals(o.name)){return this.age - o.age;}return this.name.compareTo(o.name);*/// 先按照年龄排序,如果年龄相同,再按照名字排序。if(this.age == o.age){return this.name.compareTo(o.name);}return this.age - o.age;}
}
测试类:
/*** 如果key是自定义类型的,能排序吗?* 默认情况下是不行的,会出现:ClassCastException* 底层会将key向下转型为:Comparable接口类型。*/
public class TreeMapTest02 {public static void main(String[] args) {// 创建Map集合Map<Person, String> persons = new TreeMap<>();// 创建PersonPerson p1 = new Person("bbc", 20);Person p2 = new Person("abc", 19);Person p3 = new Person("bbb", 5);Person p4 = new Person("ccc", 25);Person p5 = new Person("aaa", 25);// 添加// java.lang.ClassCastException// class Person cannot be cast to class java.lang.Comparablepersons.put(p1, "1");persons.put(p2, "2");persons.put(p3, "3");persons.put(p4, "4");persons.put(p5, "5");System.out.println(persons);}
}
⑵创建TreeMap集合时,传一个比较器,比较器实现Comparator接口,在compare方法中添加比较规则。
User类:
import java.util.Objects;public class User {private String name;private int age;public User(String name, int age) {this.name = name;this.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;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;User user = (User) o;return age == user.age && Objects.equals(name, user.name);}@Overridepublic int hashCode() {return Objects.hash(name, age);}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
UserComparater类;
/*** 单独的比较器。如果比较规则会发生变化,建议单独编写一个比较器。这样扩展能力强,更加符合OCP原则。*/
public class UserComparator implements Comparator<User> {@Overridepublic int compare(User o1, User o2) {//return o1.getAge() - o2.getAge(); // o1在前是升序。return o2.getAge() - o1.getAge();}}
测试类;
/*** 使用比较器的方式完成排序。*/
public class TreeMapTest03 {public static void main(String[] args) {/*// 创建一个比较器对象UserComparator comparator = new UserComparator();// 创建TreeMap集合的时候,可以给构造方法传递一个比较器。Map<User,String> map = new TreeMap<>(comparator);*///上面2步合成一步// 创建Map集合Map<User,String> map = new TreeMap<>(new UserComparator());User user1 = new User("zhangsan1", 20);User user2 = new User("zhangsan2", 2);User user3 = new User("zhangsan3", 10);User user4 = new User("zhangsan4", 18);User user5 = new User("zhangsan5", 9);map.put(user1, "1");map.put(user2, "1");map.put(user3, "1");map.put(user4, "1");map.put(user5, "1");System.out.println(map);// 匿名内部类方式Map<User, Integer> map2 = new TreeMap<>(new Comparator<User>() {@Overridepublic int compare(User o1, User o2) {return o1.getAge() - o2.getAge();}});map2.put(user1, 1);map2.put(user2, 1);map2.put(user3, 1);map2.put(user4, 1);map2.put(user5, 1);System.out.println(map2);}
}
运行结果:
六、Java哪些集合不能添加NULL
在Java中,某些集合类不允许添加null
元素,主要原因是数据结构的设计需求(如排序依赖比较)或并发场景下的安全性考虑。以下是常见的不允许添加null
的集合类及其原因:
1. TreeSet
和 TreeMap
-
原因:基于红黑树实现,依赖自然排序或自定义比较器(
Comparator
)。null
无法进行比较,导致无法确定节点位置。TreeSet<String> treeSet = new TreeSet<>(); treeSet.add(null); // 抛出 NullPointerExceptionTreeMap<String, String> treeMap = new TreeMap<>(); treeMap.put(null, "value"); // 抛出 NullPointerException
2. PriorityQueue
-
原因:基于堆结构,元素必须可比较。
null
无法参与比较。PriorityQueue<String> pq = new PriorityQueue<>(); pq.add(null); // 抛出 NullPointerException
3. ArrayDeque
-
原因:设计上不允许
null
元素,可能因poll()
方法返回null
表示空队列,避免歧义。ArrayDeque<String> deque = new ArrayDeque<>(); deque.add(null); // 抛出 NullPointerException
4. 并发集合类
-
ConcurrentHashMap
(Java 8+):键和值均不能为null
,避免并发场景下的歧义。 -
Hashtable
:键和值均不能为null
(设计如此)。 -
ConcurrentLinkedQueue
等并发队列:通常不允许null
元素。ConcurrentHashMap<String, String> chm = new ConcurrentHashMap<>(); chm.put("key", null); // 抛出 NullPointerExceptionHashtable<String, String> ht = new Hashtable<>(); ht.put(null, "value"); // 抛出 NullPointerException
5. EnumSet
和 EnumMap
-
EnumSet
:元素为枚举类型,枚举实例不能为null
。 -
EnumMap
:键不能为null
(必须为枚举类型)。enum Color { RED, GREEN } EnumSet<Color> enumSet = EnumSet.of(Color.RED); enumSet.add(null); // 抛出 NullPointerExceptionEnumMap<Color, String> enumMap = new EnumMap<>(Color.class); enumMap.put(null, "value"); // 抛出 NullPointerException
允许null
的常见集合
-
List:
ArrayList
、LinkedList
、Vector
。 -
Set:
HashSet
、LinkedHashSet
。 -
Map:
HashMap
、LinkedHashMap
(允许一个null
键,多个null
值)。 -
并发集合:
CopyOnWriteArrayList
、CopyOnWriteArraySet
(允许null
)。
总结
不允许null
的集合通常与排序、比较或并发安全相关。在使用时需注意具体类的文档说明,避免因NullPointerException
导致程序异常。
七、Set接口
Set接口继承Collection,没有任何新增任何方法。
Set接口常用实现类包括:HashSet、LinkedHashSet、TreeSet。
通过源码得知:HashSet底层就是HashMap,往HashSet集合中存储元素,实际上是放到了HashMap集合的key部分。因此放在HashSet集合中的元素,要同时重写hashCode+equals。底层当然也是哈希表。HashSet集合存储元素特点:无序不可重复。
通过源码得知:LinkedHashSet底层就是LinkedHashMap。所以底层是“哈希表+双向链表”。LinkedHashSet集合存储元素特点:有序不可重复。有序指的是存进去的顺序和取出的顺序一样。放进去的元素也需要重写hashCode+equals。
通过源码得知:TreeSet底层就是TreeMap。所以底层也是红黑树。TreeSet集合存储元素特点:有序不可重复。有序表示可排序。放在TreeSet集合中元素要想排序,要么存储的元素实现Comparable接口,要么在构造TreeSet集合的时候传一个比较器。TreeSet中不能存放null。
八、HashSet面试题
HashSet<Student> set = new HashSet<>();
Student stu = new Student("张三", 18);
set.add(stu);
set.add(new Student("李四", 21));
stu.setName("王五");
// 问题1:请问是否删除了HashSet集合中的stu对象呢???
set.remove(stu);
// 问题2:添加以下Student对象是否成功???
set.add(new Student("王五", 18));
// 问题3:添加以下Student对象是否成功???
set.add(new Student("张三", 18));
Student
类的实现
import java.util.Objects;public class Student {private String name;private int age;@Overridepublic 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 && Objects.equals(name, student.name);}@Overridepublic int hashCode() {return Objects.hash(name, age);}public Student(String name, int age) {this.name = name;this.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;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}
}
HashSetExam
类的实现及面试题解答
import java.util.HashSet;/*** HashSet面试题*/
public class HashSetExam {public static void main(String[] args) {// 创建HashSet集合(底层HashMap,哈希表数据结构)HashSet<Student> set = new HashSet<>();// 创建Student对象Student stu = new Student("张三", 18);// 添加Student对象set.add(stu);// 又添加了新的Student对象set.add(new Student("李四", 21));System.out.println(set);// 将张三学生的名字修改为王五// 虽然修改了,但是这个节点Node还是采用了之前 张三 的哈希值stu.setName("王五");// 问题1:请问是否删除了HashSet集合中的stu对象呢???// 不能删除set.remove(stu);//System.out.println(set);// 问题2:添加以下Student对象是否成功???// 可以添加成功set.add(new Student("王五", 18));//System.out.println(set);// 问题3:添加以下Student对象是否成功???// 可以添加成功set.add(new Student("张三", 18));System.out.println(set);}
}
运行结果:
1.代码逻辑解释
-
创建
HashSet
集合并添加元素:HashSet<Student> set = new HashSet<>();
:创建一个HashSet
集合,用于存储Student
对象。Student stu = new Student("张三", 18);
:创建一个Student
对象stu
,姓名为 "张三",年龄为 18。set.add(stu);
:将stu
对象添加到HashSet
集合中。此时,HashSet
会根据stu
对象的hashCode
方法计算出的哈希值,将其存储到相应的位置。set.add(new Student("李四", 21));
:创建一个新的Student
对象,姓名为 "李四",年龄为 21,并将其添加到HashSet
集合中。
-
修改
Student
对象的属性:stu.setName("王五");
:将stu
对象的姓名修改为 "王五"。需要注意的是,HashSet
中的节点仍然使用的是修改前对象的哈希值,因为哈希值在对象添加到HashSet
时就已经确定了。
-
问题 1:
set.remove(stu);
是否删除了HashSet
集合中的stu
对象?- 答案是不能删除。
HashSet
在进行删除操作时,首先会根据对象的hashCode
方法计算出的哈希值找到对应的位置,然后再通过equals
方法比较对象是否相等。由于stu
对象的姓名已经修改,但其在HashSet
中的存储位置仍然是根据修改前的哈希值确定的,而现在stu
对象的哈希值已经发生了变化,HashSet
会根据新的哈希值去查找,自然找不到对应的元素,所以删除操作失败。
- 答案是不能删除。
-
问题 2:
set.add(new Student("王五", 18));
是否添加成功?- 答案是可以添加成功。
HashSet
在添加元素时,同样会根据元素的hashCode
方法计算出的哈希值找到对应的位置,然后检查该位置是否已经存在相同的元素(通过equals
方法比较)。由于新添加的Student
对象的哈希值与之前修改后的stu
对象的哈希值不同,HashSet
会认为这是一个新的元素,所以可以添加成功。
- 答案是可以添加成功。
-
问题 3:
set.add(new Student("张三", 18));
是否添加成功?- 答案是可以添加成功。虽然新添加的
Student
对象的姓名和年龄与最初的stu
对象相同,但是由于之前的stu
对象的姓名已经修改,其在HashSet
中的存储位置对应的元素已经不是最初的stu
对象了,所以HashSet
会认为这是一个新的元素,从而添加成功。
- 答案是可以添加成功。虽然新添加的
2.详细解释与分析
以下是对代码及三个问题的逐步分析,假设 Student
类已正确覆盖 hashCode()
和 equals()
方法(基于 name
和 age
字段)。
代码执行流程
⑴初始化集合:
HashSet<Student> set = new HashSet<>();
Student stu = new Student("张三", 18);
set.add(stu);
set.add(new Student("李四", 21));
-
set
中包含两个元素:-
Student("张三", 18)
(哈希值由 "张三" 和 18 计算) -
Student("李四", 21)
(哈希值由 "李四" 和 21 计算)。
-
⑵修改 stu
的 name
:
stu.setName("王五");
-
stu
的name
从 "张三" 变为 "王五",导致其hashCode()
值改变。 -
关键问题:
HashSet
存储的是对象的引用,但哈希表的存储位置(桶)由初始的hashCode()
决定。修改字段后,哈希值变化,但HashSet
不会自动更新存储位置。
问题1:set.remove(stu)
能否删除原对象?
答案:删除失败
原因:
-
存储机制:
-
stu
最初以name="张三"
和age=18
的哈希值存储在哈希表的某个桶中。 -
修改
name
为 "王五" 后,stu
的哈希值变为基于 "王五" 和 18 的新值。
-
-
删除逻辑:
-
set.remove(stu)
会基于新的哈希值("王五" 和 18)查找对应的桶。 -
原对象存储在旧的桶(基于 "张三" 和 18 的哈希值),因此无法找到,删除失败。
-
-
验证结果:
System.out.println(set.size()); // 输出 2(删除失败,集合仍有两个元素)
问题2:添加 new Student("王五", 18)
是否成功?
答案:添加成功
原因:
-
当前集合状态:
-
set
中存在修改后的stu
对象(name="王五"
,age=18
),但其哈希值已变化。
-
-
对象比较:
-
新对象
new Student("王五", 18)
的hashCode()
和equals()
与stu
完全相同。 -
预期结果:由于
HashSet
不允许重复,新对象应被拒绝。
-
-
矛盾点:
-
修改后的
stu
对象仍存储在旧的桶(基于初始哈希值 "张三" 和 18)。 -
新对象
new Student("王五", 18)
会基于新的哈希值存储到不同的桶中。 -
哈希表逻辑:即使两个对象
equals()
返回true
,若哈希值不同,HashSet
会将它们视为不同对象(因为哈希冲突未发生)。
-
-
实际结果:
System.out.println(set.size()); // 输出 3(添加成功)
-
由于
stu
的存储位置未更新,新对象被添加到新的桶中,导致重复元素存在。
-
问题3:添加 new Student("张三", 18)
是否成功?
答案:添加成功
原因:
-
当前集合状态:
-
set
中存在:-
修改后的
stu
(name="王五"
,age=18
,哈希值已变化)。 -
new Student("李四", 21)
。
-
-
-
对象比较:
-
新对象
new Student("张三", 18)
的哈希值与初始的stu
对象(修改前)相同。 -
由于原
stu
的name
已被修改,新对象与当前set
中的元素均不同。
-
-
存储逻辑:
-
新对象会存储在基于 "张三" 和 18 的哈希值的桶中。
-
该桶中可能已无元素(原
stu
的哈希值已改变),因此添加成功。
-
-
验证结果:
System.out.println(set.size()); // 输出 4(添加成功)
总结与根本原因
-
HashSet
的底层机制:-
依赖
hashCode()
确定存储位置,依赖equals()
处理哈希冲突。 -
若对象属性被修改导致哈希值变化,
HashSet
不会更新存储位置。
-
-
关键结论:
-
禁止修改已存储在
HashSet
中的对象的hashCode()
依赖字段,否则会导致:-
删除/查找失败(问题1)。
-
重复元素被误添加(问题2)。
-
哈希表逻辑混乱(问题3)。
-
-
建议将
hashCode()
和equals()
基于不可变字段(如数据库主键id
)。
-
验证代码输出
// 初始添加后
System.out.println(set); // [Student{name='张三', age=18}, Student{name='李四', age=21}]// 修改后尝试删除
stu.setName("王五");
set.remove(stu);
System.out.println(set); // [Student{name='王五', age=18}, Student{name='李四', age=21}]// 添加 new Student("王五", 18)
set.add(new Student("王五", 18));
System.out.println(set); // [Student{name='王五', age=18}, Student{name='李四', age=21}, Student{name='王五', age=18}]// 添加 new Student("张三", 18)
set.add(new Student("张三", 18));
System.out.println(set); // [Student{name='王五', age=18}, Student{name='李四', age=21}, Student{name='王五', age=18}, Student{name='张三', age=18}]
最终建议
-
不要依赖可变字段实现
hashCode()
和equals()
,否则会导致HashSet
/HashMap
行为不可预测。 -
若需修改对象的关键字段,应先从集合中移除对象,修改后再重新添加。
九、Collections工具类
1. sort
方法
sort
方法用于对 List
集合中的元素进行排序。Collections
类提供了两种重载的 sort
方法:
public static <T extends Comparable<? super T>> void sort(List<T> list)
:使用元素的自然顺序对List
进行排序,要求元素类必须实现Comparable
接口。public static <T> void sort(List<T> list, Comparator<? super T> c)
:使用指定的比较器Comparator
对List
进行排序。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;public class SortExample {public static void main(String[] args) {List<Integer> numbers = new ArrayList<>();numbers.add(5);numbers.add(2);numbers.add(8);numbers.add(1);// 使用自然顺序排序Collections.sort(numbers);System.out.println("自然顺序排序结果: " + numbers);// 使用自定义比较器进行降序排序Collections.sort(numbers, (a, b) -> b - a);System.out.println("降序排序结果: " + numbers);}
}
2. shuffle
方法
shuffle
方法用于随机打乱 List
集合中元素的顺序。它有两种重载形式:
public static void shuffle(List<?> list)
:使用默认的随机源对List
进行混排。public static void shuffle(List<?> list, Random rnd)
:使用指定的随机源Random
对List
进行混排。import java.util.ArrayList; import java.util.Collections; import java.util.List;public class ShuffleExample {public static void main(String[] args) {List<Integer> numbers = new ArrayList<>();for (int i = 1; i <= 10; i++) {numbers.add(i);}System.out.println("原始顺序: " + numbers);// 打乱顺序Collections.shuffle(numbers);System.out.println("打乱后的顺序: " + numbers);} }
运行结果:
3. reverse
方法
reverse
方法用于反转 List
集合中元素的顺序。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;public class ReverseExample {public static void main(String[] args) {List<String> names = new ArrayList<>();names.add("Alice");names.add("Bob");names.add("Charlie");System.out.println("原始顺序: " + names);// 反转顺序Collections.reverse(names);System.out.println("反转后的顺序: " + names);}
}
运行结果;
4. fill
方法
fill
方法用于将 List
集合中的所有元素替换为指定的元素。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;public class FillExample {public static void main(String[] args) {List<String> fruits = new ArrayList<>();fruits.add("Apple");fruits.add("Banana");fruits.add("Cherry");System.out.println("原始列表: " + fruits);// 替换所有元素Collections.fill(fruits, "Grape");System.out.println("替换后的列表: " + fruits);}
}
运行结果:
相关文章:

Java集合框架全解析:从LinkedHashMap到TreeMap与HashSet面试题实战
一、LinkedHashMap ①LinkedHashMap集合和HashMap集合的用法完全相同。 ②不过LinkedHashMap可以保证插入顺序。 ③LinkedHashMap集合因为可以保证插入顺序,因此效率比HashMap低一些。 ④LinkedHashMap是如何保证插入顺序的? 底层采用了双向链表来记…...

goland无法debug项目
1、其实个原因是因为正在使用的Delve调试器版本太旧,无法兼容当前的Go语言版本1.2。Delve是Go语言的一个调试工具,用于提供源码级别的调试功能。Go语言每隔一段时间会发布新版本,而相应的调试器Delve也可能会更新以提供新的特性或修复已知问题…...
深入探索 DeepSeek 在数据分析与可视化中的应用
在数据驱动的时代,快速且准确地分析和呈现数据对于企业和个人都至关重要。DeepSeek 作为一款先进的人工智能工具,凭借其强大的数据处理和可视化能力,正在革新数据分析的方式。 1. 数据预处理与清洗 在进行数据分析前,数据预处理…...

python面试题整理
Python 如何处理异常? Python中,使用try 和 except 关键字来捕获和处理异常 try 块中放置可能会引发异常的代码,然后在except块中处理这些异常。 能补充一下finally的作用吗? finally 块中的代码无论是否发生异常都会执行…...

大型装备故障诊断解决方案
大型装备故障诊断解决方案 方案背景 在全球航空工业迅猛发展的背景下,我国在军用和民用飞机自主研发制造领域取得了显著成就。尤其是在国家大力支持下,国内飞机制造企业攻克了诸多关键技术难题,实现了从设计研发到生产制造再到售后保障的完整…...

基于SpringBoot+vue+uniapp的智慧旅游小程序+LW示例参考
系列文章目录 1.基于SSM的洗衣房管理系统原生微信小程序LW参考示例 2.基于SpringBoot的宠物摄影网站管理系统LW参考示例 3.基于SpringBootVue的企业人事管理系统LW参考示例 4.基于SSM的高校实验室管理系统LW参考示例 5.基于SpringBoot的二手数码回收系统原生微信小程序LW参考示…...

小怿学习日记(七) | Unreal引擎灯光架构
灯光的布局对于HMI场景中车模的展示效果有着举足轻重的地位。本篇内容将简单介绍ES3.1的相关知识,再深入了解Unreal引擎中车模的灯光以及灯光架构。 一、关于ES3.1 1.1 什么是ES3.1 ES3.1这个概念对于美术的同学可能比较陌生,ES3.1指的是OpenGL ES3.1&…...
网络运维学习笔记 013网工初级(HCIA-Datacom与CCNA-EI)DHCP动态主机配置协议(此处只讲华为)
文章目录 DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)网关配置DHCP服务器配置如果没有DHCP服务器,只在网关上做DHCP服务器: DHCP(Dynamic Host Configuration Protocol,动态主…...

javaEE-14.spring MVC练习
目录 1.加法计算器 需求分析: 前端页面代码: 后端代码实现功能: 调整前端页面代码: 进行测试: 2.用户登录 需求分析: 定义接口: 1.登录数据校验接口: 2.查询登录用户接口: 前端代码: 后端代码: 调整前端代码: 测试/查错因 后端: 前端: lombok工具 1.引入依赖…...
HTML/CSS中并集选择器
1.作用:选中多个选择器对应的元素,又称:分组选择器 所谓并集就是或者的含义. 2.语法:选择器1,选择器2,选择器3,......选择器n 多个选择器通过,连接,此处,的含义就是:或. .rich,.beauty{color: blue;} 3.注意事项 1.并集选择器,我们一般竖着写 2.任何形式的选择器,都可以作为并…...

可编辑35页PPT | DeepSeek如何赋能职场应用
DeepSeek作为一款基于人工智能技术的多模态AI工具,近年来在职场应用中迅速崭露头角,以其强大的功能和高效的表现赢得了广泛的赞誉。DeepSeek融合了文本生成、图像创作、数据分析与可视化等多种功能,为职场人士提供了前所未有的便利和效率提升…...
CSS `transform` 属性详解:打造视觉效果与动画的利器
CSS transform 属性详解:打造视觉效果与动画的利器 引言一、transform 属性简介二、平移(Translation)三、旋转(Rotation)四、缩放(Scale)五、倾斜(Skew)六、组合变换&am…...
Windows使用docker部署fastgpt出现的一些问题
文章目录 Windows使用docker部署FastGPT出现的一些问题1.docker部署pg一直重启的问题2.重启MongoDB之后一直出现“Waiting for MongoDB to start...”3.oneapi启动不了failed to get gpt-3.5-turbo token encoder Windows使用docker部署FastGPT出现的一些问题 1.docker部署pg一…...
Spring Boot中如何使用Thymeleaf模板引擎
Thymeleaf 是一个现代化的服务器端 Java 模板引擎,在 Spring Boot 项目中使用它可以方便地将 Java 代码和 HTML 页面进行整合,生成动态的 Web 页面。以下将详细介绍在 Spring Boot 中如何使用 Thymeleaf 模板引擎。 1. 添加依赖 如果你使用的是 Maven 项目,在 pom.xml 中添…...

C/C++跳动的爱心
系列文章 序号直达链接1C/C李峋同款跳动的爱心2C/C跳动的爱心3C/C经典爱心4C/C满屏飘字5C/C大雪纷飞6C/C炫酷烟花7C/C黑客帝国同款字母雨8C/C樱花树9C/C奥特曼10C/C精美圣诞树11C/C俄罗斯方块小游戏12C/C贪吃蛇小游戏13C/C孤单又灿烂的神14C/C闪烁的爱心15C/C哆啦A梦16C/C简单…...
Element UI中messageBox怎么区分点击取消按钮关闭弹窗,和点击右上角x号以及点击遮罩层关闭按钮
在某些场景下,我们可能需要区分点击取消按钮关闭 messageBox 和点击X号、遮罩层关闭 messageBox 。 实现: 将 distinguishCancelAndClose 设置为 true,这个属性的意思是:是否将取消(点击取消按钮)与关闭&…...
Websocket——心跳检测
1. 前言:为什么需要心跳机制? 在现代的实时网络应用中,保持客户端和服务端的连接稳定性是非常重要的。尤其是在长时间的网络连接中,存在一些异常情况,导致服务端无法及时感知到客户端的断开,可能造成不必要…...

Javascript使用Sodium库实现 aead_xchacha20poly1305_ietf加密解密,以及与后端的密文交互
Node.js环境安装 sodium-native (其他库可能会出现加密解密失败,如果要使用不一样的库,请自行验证) npm install sodium-native 示例代码,使用的是 sodium-native v4.3.2 (其他版本可能会有变化,如果要使用,请自行验…...
视频mp4垂直拼接 水平拼接
视频mp4垂直拼接 水平拼接 pinjie_v.py import imageio import numpy as np import os import cv2def pinjie_v(dir1,dir2,out_dir):os.makedirs(out_dir, exist_okTrue)# 获取目录下的所有视频文件video_files_1 [f for f in os.listdir(dir1) if f.endswith(.mp4)]video_fi…...

跟着李沐老师学习深度学习(十二)
循环神经网络 序列模型 序列数据 实际中很多数据是有时序结构的 比如:电影的评价随时间变化而变化 拿奖后评分上升,直到奖项被忘记看了很多好电影后,人们的期望变高季节性:贺岁片、暑期档导演、演员的负面报道导致评分变低 核心思想&#…...

调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...

基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...

汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...

mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...

使用SSE解决获取状态不一致问题
使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件,这个上传文件是整体功能的一部分,文件在上传的过程中…...
Spring Boot + MyBatis 集成支付宝支付流程
Spring Boot MyBatis 集成支付宝支付流程 核心流程 商户系统生成订单调用支付宝创建预支付订单用户跳转支付宝完成支付支付宝异步通知支付结果商户处理支付结果更新订单状态支付宝同步跳转回商户页面 代码实现示例(电脑网站支付) 1. 添加依赖 <!…...

STM32标准库-ADC数模转换器
文章目录 一、ADC1.1简介1. 2逐次逼近型ADC1.3ADC框图1.4ADC基本结构1.4.1 信号 “上车点”:输入模块(GPIO、温度、V_REFINT)1.4.2 信号 “调度站”:多路开关1.4.3 信号 “加工厂”:ADC 转换器(规则组 注入…...