一、HashSet源码简析
HashSet
是 Java 集合框架中的一个重要类,它实现了 Set
接口,用于存储不重复的元素。HashSet
内部实际上是通过 HashMap
来实现的,其中每个元素都是 HashMap
的一个键,而值则是一个固定的对象(通常是 PRESENT
,一个 HashSet
的静态成员变量)。这种实现方式使得 HashSet
的添加、删除和查找操作都具有接近常数时间的性能。
下面是一个简化的 HashSet
源码剖析,以帮助你理解其内部工作机制:
成员变量
// HashMap 用于存储元素
private transient HashMap<E,Object> map;// 一个用于表示 HashSet 中值的对象
private static final Object PRESENT = new Object();
构造函数
// 默认构造函数,创建一个空的 HashSet
public HashSet() {map = new HashMap<>();
}// 带有初始容量的构造函数
public HashSet(int initialCapacity) {map = new HashMap<>(initialCapacity);
}// 带有初始容量和加载因子的构造函数
public HashSet(int initialCapacity, float loadFactor) {map = new HashMap<>(initialCapacity, loadFactor);
}
添加元素
public boolean add(E e) {return map.put(e, PRESENT) == null;
}
当调用 add
方法时,实际上是在 HashMap
中添加了一个键值对,其中键是要添加的元素,值是 PRESENT
。如果添加成功(即之前该键不存在于 HashMap
中),则返回 true
;否则返回 false
。
删除元素
public boolean remove(Object o) {return map.remove(o) == PRESENT;
}
删除操作实际上是在 HashMap
中删除指定的键。如果删除成功且被删除的值是 PRESENT
,则返回 true
;否则返回 false
。
查找元素
public boolean contains(Object o) {return map.containsKey(o);
}
查找操作实际上是检查指定的键是否存在于 HashMap
中。
遍历元素
遍历 HashSet
中的元素通常通过 Iterator
或增强型 for
循环来完成,这些操作最终都是基于 HashMap
的键集合的遍历。
注意事项
- 由于
HashSet
不保证元素的迭代顺序,因此每次遍历HashSet
时,元素的顺序可能会有所不同。 - 由于
HashSet
是基于HashMap
实现的,因此其性能特性(如时间复杂度)与HashMap
相似。添加、删除和查找操作的时间复杂度通常是常数时间,但在极端情况下(如哈希冲突很多时)可能会退化为线性时间。
这只是一个简化的源码剖析,实际的 HashSet
实现可能包含更多的优化和特性。为了更深入地了解 HashSet
的工作原理,建议查阅 Java 官方文档或源代码。
二、在 HashSet 中将 Object PRESENT = new Object() 设置为 static final 属性主要有以下几个原因:
在 HashSet
中将 Object PRESENT = new Object()
设置为 static final
属性主要有以下几个原因:
-
节约内存空间:
PRESENT
对象在HashSet
中用作所有元素的“值”。由于HashSet
的内部实现是基于HashMap
的,而HashMap
存储的是键值对,HashSet
需要一个占位符作为值。通过将PRESENT
设置为static final
,这个对象只会在类加载时创建一次,并被所有HashSet
实例共享。这避免了为每个HashSet
实例都创建一个新的Object
实例,从而节约了内存空间。 -
保证值的不变性:由于
PRESENT
是一个final
对象,它的引用是不可变的。这意味着一旦PRESENT
被赋值,就不能再被重新赋值指向其他对象。这确保了HashSet
的内部实现不会因为PRESENT
值的改变而出错。 -
代码可读性:使用
PRESENT
作为占位符值可以使代码更加清晰和易于理解。当其他开发者看到PRESENT
被用作值时,他们可以立即理解这是HashSet
的一个内部实现细节,而不是一个需要特别关注的业务逻辑值。 -
线程安全:虽然
HashSet
本身不是线程安全的,但将PRESENT
设置为static final
可以确保在多线程环境下,PRESENT
的引用不会被意外修改。这有助于减少并发编程中的潜在错误。
综上所述,将 Object PRESENT = new Object()
设置为 static final
属性是出于性能、代码可读性和线程安全性的考虑。这种设计使得 HashSet
的内部实现更加高效和健壮。
三、是不是只要设置为static的对象,那么这个对象只会在类加载时创建一次?并被所有这个类实例共享
是的,您的理解是正确的。在Java中,当一个字段(无论是基本类型还是对象引用)被声明为static
时,这个字段就属于类本身,而不是类的任何特定实例。因此,static
字段的初始化只会在类首次被加载到JVM时发生一次,而不是每次创建类的新实例时都发生。
具体来说:
- 当类被加载到JVM时,它的
static
字段会被初始化。如果是引用类型,那么会分配内存空间,并赋予默认值(对于对象引用类型,默认值是null
)。然后,如果提供了显式的初始化表达式或初始化块,这些会按照它们在代码中出现的顺序被执行。 - 一旦
static
字段被初始化,它就会一直存在于内存中,直到类被卸载(这通常发生在类加载器被垃圾回收时)。 - 类的所有实例都共享相同的
static
字段。因此,通过一个实例修改static
字段的值会影响到所有其他实例。
例如:
public class MyClass {public static int staticVar = 42;public int instanceVar;public MyClass(int value) {instanceVar = value;}public static void main(String[] args) {MyClass obj1 = new MyClass(10);MyClass obj2 = new MyClass(20);System.out.println(MyClass.staticVar); // 输出 42,因为staticVar是静态的,只有一个值MyClass.staticVar = 50; // 修改静态变量的值System.out.println(obj1.staticVar); // 输出 50,因为obj1和obj2共享这个静态变量System.out.println(obj2.staticVar); // 输出 50// 注意:实例变量不会被共享System.out.println(obj1.instanceVar); // 输出 10System.out.println(obj2.instanceVar); // 输出 20}
}
在这个例子中,staticVar
是MyClass
的一个static
字段,它在类加载时初始化,并且obj1
和obj2
两个实例共享这个字段。当我们通过MyClass.staticVar = 50;
修改它的值时,这个修改会反映在所有通过MyClass
访问这个字段的地方。相反,instanceVar
是实例字段,每个MyClass
的实例都有它自己的副本。