当前位置: 首页 > news >正文

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的常见集合

  • ListArrayListLinkedListVector

  • SetHashSetLinkedHashSet

  • MapHashMapLinkedHashMap(允许一个null键,多个null值)。

  • 并发集合CopyOnWriteArrayListCopyOnWriteArraySet(允许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.代码逻辑解释

  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 集合中。
  2. 修改 Student 对象的属性

    • stu.setName("王五");:将 stu 对象的姓名修改为 "王五"。需要注意的是,HashSet 中的节点仍然使用的是修改前对象的哈希值,因为哈希值在对象添加到 HashSet 时就已经确定了。
  3. 问题 1:set.remove(stu); 是否删除了 HashSet 集合中的 stu 对象?

    • 答案是不能删除。HashSet 在进行删除操作时,首先会根据对象的 hashCode 方法计算出的哈希值找到对应的位置,然后再通过 equals 方法比较对象是否相等。由于 stu 对象的姓名已经修改,但其在 HashSet 中的存储位置仍然是根据修改前的哈希值确定的,而现在 stu 对象的哈希值已经发生了变化,HashSet 会根据新的哈希值去查找,自然找不到对应的元素,所以删除操作失败。
  4. 问题 2:set.add(new Student("王五", 18)); 是否添加成功?

    • 答案是可以添加成功。HashSet 在添加元素时,同样会根据元素的 hashCode 方法计算出的哈希值找到对应的位置,然后检查该位置是否已经存在相同的元素(通过 equals 方法比较)。由于新添加的 Student 对象的哈希值与之前修改后的 stu 对象的哈希值不同,HashSet 会认为这是一个新的元素,所以可以添加成功。
  5. 问题 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) 能否删除原对象?

答案:删除失败
原因

  1. 存储机制

    • stu 最初以 name="张三" 和 age=18 的哈希值存储在哈希表的某个桶中。

    • 修改 name 为 "王五" 后,stu 的哈希值变为基于 "王五" 和 18 的新值。

  2. 删除逻辑

    • set.remove(stu) 会基于新的哈希值("王五" 和 18)查找对应的桶。

    • 原对象存储在旧的桶(基于 "张三" 和 18 的哈希值),因此无法找到,删除失败。

  3. 验证结果

    System.out.println(set.size()); // 输出 2(删除失败,集合仍有两个元素)

问题2:添加 new Student("王五", 18) 是否成功?

答案:添加成功
原因

  1. 当前集合状态

    • set 中存在修改后的 stu 对象(name="王五"age=18),但其哈希值已变化。

  2. 对象比较

    • 新对象 new Student("王五", 18) 的 hashCode() 和 equals() 与 stu 完全相同。

    • 预期结果:由于 HashSet 不允许重复,新对象应被拒绝。

  3. 矛盾点

    • 修改后的 stu 对象仍存储在旧的桶(基于初始哈希值 "张三" 和 18)。

    • 新对象 new Student("王五", 18) 会基于新的哈希值存储到不同的桶中。

    • 哈希表逻辑:即使两个对象 equals() 返回 true,若哈希值不同,HashSet 会将它们视为不同对象(因为哈希冲突未发生)。

  4. 实际结果

    System.out.println(set.size()); // 输出 3(添加成功)
    • 由于 stu 的存储位置未更新,新对象被添加到新的桶中,导致重复元素存在。


问题3:添加 new Student("张三", 18) 是否成功?

答案:添加成功
原因

  1. 当前集合状态

    • set 中存在:

      • 修改后的 stuname="王五"age=18,哈希值已变化)。

      • new Student("李四", 21)

  2. 对象比较

    • 新对象 new Student("张三", 18) 的哈希值与初始的 stu 对象(修改前)相同。

    • 由于原 stu 的 name 已被修改,新对象与当前 set 中的元素均不同。

  3. 存储逻辑

    • 新对象会存储在基于 "张三" 和 18 的哈希值的桶中。

    • 该桶中可能已无元素(原 stu 的哈希值已改变),因此添加成功。

  4. 验证结果

    System.out.println(set.size()); // 输出 4(添加成功)

总结与根本原因

  1. HashSet 的底层机制

    • 依赖 hashCode() 确定存储位置,依赖 equals() 处理哈希冲突。

    • 若对象属性被修改导致哈希值变化,HashSet 不会更新存储位置

  2. 关键结论

    • 禁止修改已存储在 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集合因为可以保证插入顺序&#xff0c;因此效率比HashMap低一些。 ④LinkedHashMap是如何保证插入顺序的&#xff1f; 底层采用了双向链表来记…...

goland无法debug项目

1、其实个原因是因为正在使用的Delve调试器版本太旧&#xff0c;无法兼容当前的Go语言版本1.2。Delve是Go语言的一个调试工具&#xff0c;用于提供源码级别的调试功能。Go语言每隔一段时间会发布新版本&#xff0c;而相应的调试器Delve也可能会更新以提供新的特性或修复已知问题…...

深入探索 DeepSeek 在数据分析与可视化中的应用

在数据驱动的时代&#xff0c;快速且准确地分析和呈现数据对于企业和个人都至关重要。DeepSeek 作为一款先进的人工智能工具&#xff0c;凭借其强大的数据处理和可视化能力&#xff0c;正在革新数据分析的方式。 1. 数据预处理与清洗 在进行数据分析前&#xff0c;数据预处理…...

python面试题整理

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

大型装备故障诊断解决方案

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

基于SpringBoot+vue+uniapp的智慧旅游小程序+LW示例参考

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

小怿学习日记(七) | Unreal引擎灯光架构

灯光的布局对于HMI场景中车模的展示效果有着举足轻重的地位。本篇内容将简单介绍ES3.1的相关知识&#xff0c;再深入了解Unreal引擎中车模的灯光以及灯光架构。 一、关于ES3.1 1.1 什么是ES3.1 ES3.1这个概念对于美术的同学可能比较陌生&#xff0c;ES3.1指的是OpenGL ES3.1&…...

网络运维学习笔记 013网工初级(HCIA-Datacom与CCNA-EI)DHCP动态主机配置协议(此处只讲华为)

文章目录 DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff09;网关配置DHCP服务器配置如果没有DHCP服务器&#xff0c;只在网关上做DHCP服务器&#xff1a; DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主…...

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工具&#xff0c;近年来在职场应用中迅速崭露头角&#xff0c;以其强大的功能和高效的表现赢得了广泛的赞誉。DeepSeek融合了文本生成、图像创作、数据分析与可视化等多种功能&#xff0c;为职场人士提供了前所未有的便利和效率提升…...

CSS `transform` 属性详解:打造视觉效果与动画的利器

CSS transform 属性详解&#xff1a;打造视觉效果与动画的利器 引言一、transform 属性简介二、平移&#xff08;Translation&#xff09;三、旋转&#xff08;Rotation&#xff09;四、缩放&#xff08;Scale&#xff09;五、倾斜&#xff08;Skew&#xff09;六、组合变换&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号以及点击遮罩层关闭按钮

在某些场景下&#xff0c;我们可能需要区分点击取消按钮关闭 messageBox 和点击X号、遮罩层关闭 messageBox 。 实现&#xff1a; 将 distinguishCancelAndClose 设置为 true&#xff0c;这个属性的意思是&#xff1a;是否将取消&#xff08;点击取消按钮&#xff09;与关闭&…...

Websocket——心跳检测

1. 前言&#xff1a;为什么需要心跳机制&#xff1f; 在现代的实时网络应用中&#xff0c;保持客户端和服务端的连接稳定性是非常重要的。尤其是在长时间的网络连接中&#xff0c;存在一些异常情况&#xff0c;导致服务端无法及时感知到客户端的断开&#xff0c;可能造成不必要…...

Javascript使用Sodium库实现 aead_xchacha20poly1305_ietf加密解密,以及与后端的密文交互

Node.js环境安装 sodium-native (其他库可能会出现加密解密失败&#xff0c;如果要使用不一样的库&#xff0c;请自行验证) npm install sodium-native 示例代码&#xff0c;使用的是 sodium-native v4.3.2 (其他版本可能会有变化&#xff0c;如果要使用&#xff0c;请自行验…...

视频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…...

跟着李沐老师学习深度学习(十二)

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

谷歌浏览器插件

项目中有时候会用到插件 sync-cookie-extension1.0.0&#xff1a;开发环境同步测试 cookie 至 localhost&#xff0c;便于本地请求服务携带 cookie 参考地址&#xff1a;https://juejin.cn/post/7139354571712757767 里面有源码下载下来&#xff0c;加在到扩展即可使用FeHelp…...

XCTF-web-easyupload

试了试php&#xff0c;php7&#xff0c;pht&#xff0c;phtml等&#xff0c;都没有用 尝试.user.ini 抓包修改将.user.ini修改为jpg图片 在上传一个123.jpg 用蚁剑连接&#xff0c;得到flag...

React Native 导航系统实战(React Navigation)

导航系统实战&#xff08;React Navigation&#xff09; React Navigation 是 React Native 应用中最常用的导航库之一&#xff0c;它提供了多种导航模式&#xff0c;如堆栈导航&#xff08;Stack Navigator&#xff09;、标签导航&#xff08;Tab Navigator&#xff09;和抽屉…...

Spring Boot 实现流式响应(兼容 2.7.x)

在实际开发中&#xff0c;我们可能会遇到一些流式数据处理的场景&#xff0c;比如接收来自上游接口的 Server-Sent Events&#xff08;SSE&#xff09; 或 流式 JSON 内容&#xff0c;并将其原样中转给前端页面或客户端。这种情况下&#xff0c;传统的 RestTemplate 缓存机制会…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)

0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述&#xff0c;后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作&#xff0c;其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...

关于nvm与node.js

1 安装nvm 安装过程中手动修改 nvm的安装路径&#xff0c; 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解&#xff0c;但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后&#xff0c;通常在该文件中会出现以下配置&…...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)

目录 1.TCP的连接管理机制&#xff08;1&#xff09;三次握手①握手过程②对握手过程的理解 &#xff08;2&#xff09;四次挥手&#xff08;3&#xff09;握手和挥手的触发&#xff08;4&#xff09;状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)

UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中&#xff0c;UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化&#xf…...

AI书签管理工具开发全记录(十九):嵌入资源处理

1.前言 &#x1f4dd; 在上一篇文章中&#xff0c;我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源&#xff0c;方便后续将资源打包到一个可执行文件中。 2.embed介绍 &#x1f3af; Go 1.16 引入了革命性的 embed 包&#xff0c;彻底改变了静态资源管理的…...

Springboot社区养老保险系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;社区养老保险系统小程序被用户普遍使用&#xff0c;为方…...