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获取value
String 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);
// 获取所有的key
Enumeration<?> 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;
}
@Override
public 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);
}
@Override
public 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<>();
// 创建Person
Person 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.Comparable
persons.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;
}
@Override
public 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);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
@Override
public 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<>();
// 创建Person
Person 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.Comparable
persons.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;
}
@Override
public 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);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
UserComparater类;
/**
* 单独的比较器。如果比较规则会发生变化,建议单独编写一个比较器。这样扩展能力强,更加符合OCP原则。
*/
public class UserComparator implements Comparator<User> {
@Override
public 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>() {
@Override
public 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); // 抛出 NullPointerException TreeMap<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); // 抛出 NullPointerException Hashtable<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); // 抛出 NullPointerException EnumMap<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;
@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 && Objects.equals(name, student.name);
}
@Override
public 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;
}
@Override
public 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);
}
}
运行结果: