集合的基本使用方法总结
三、 HashSet 集合的使用方法
3.1、几个HashSet使用的注意事项:
① HashSet 类是按照哈希算法来存储集合中的元素的。虽然Set集合存放元素在地址(底层)上是无序的,不能从底层上进行O(1)的随机访问、随机存取。但是由于按照哈希值存储元素,就相当于给元素带上了一个index,这个index就是哈希值。所以HashSet具有良好的存取、查找性能,比基于红黑树、平衡树的存储:TreeSet要好很多。但是这种HashSet是无序的,如果需要排序,最好就还是用TreeSet了,因为平衡树的建树过程就自动地排序、去重了!
② HashSet的对象是可以存储 null 的!这个十分需要注意,如果不小心在存储的过程中存入了 null ,就不会报错,那么在使用的时候, 就很可能空指针异常!
③ 这种集合的操作是线程不安全的,所谓线程不安全,就是对资源的访问控制有问题。当多线程竞争使用某资源的时候,不能做到线程同步、资源互斥!所以可以使用锁、或者写时拷贝的方式进行(Java自带写时拷贝的类!)
④ 这是最重要的一点!! 那就是,对于没有重写 equals() 和 hashcode() 的类,不能随便使用HashSet 去存储,因为 Set 的去重是根据判断集合中是否有另一个元素 e’ 满足 e’.equals(e) == true 来实现的!!所以如果没有重写equals() 方法,就会去比较引用指向的对象的地址!之所以还要实现hashcode() 方法是为了更高效地比较,因为hashcode() 先判断,如果 hash 值都不相等,则肯定两个对象不同,否则再执行 equals() 方法(有可能哈希碰撞,所以即使hash值相等也要判断equals() )
3.2、看一个简单的例子:
package SetInterface;
import java.util.*;
/**
* @author jiangzl
*/
public class MyHashSet {
public static void main(String[] args){
HashSet mySet = new HashSet();
mySet.add(null);
mySet.add(123);
mySet.add("hello");
mySet.add(new String("hello"));
mySet.add(new Student("jiangzl", 92));
mySet.add(new Student("jiangzl", 92));
Iterator iterator = mySet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
/**
* @author jiangzl
*/
class Student{
private String name;
private int score;
public Student(String name, int score){
this.name = name;
this.score = score;
}
@Override
public String toString(){
return new String("姓名:" + name + " , 分数:" + score);
}
}
第一:先来看看输出结果
第二,分析一下这个例子
① null 空指针是可以存放到HashSet里面去的,输出也可以输出,显示的就是 null ,但是轻易不要去操作它,建议在访问HashSet的元素的时候,来个判空,避免空指针异常!
② 说好的 HashSet 具有去重的功能,但是为何两个 Student 对象,成员变量完全一样,却保存在同一个 HashSet 中呢?原因我们上面已经提过了,因为 HashSet 判重是根据hashcode() 和 equals() 方法做的。不妨看看阿里巴巴的代码规约如何提示:
③ 为何两个 “hello” 的String 对象没有在HashSet 中同时保存呢?哪怕都是 new 出来的对象,或者是字符串常量池的对象,他们都是String类的对象,String类是重写了hashcode() 和 equals() 方法的!所以会被判重机制算作重复!
第三,改进一下这个例子
如何重写 equals() 方法
这个问题其实是个很简单的问题,最好的建议就是判断每个成员变量是否相等即可。不够我们最好是合理利用一些既有的类的方法去处理,比如你如果有个String类的成员对象,那么最好是先判断hashcode(),再判断是否equals()
如何重写 hashcode() 方法
这个问题就是一个很值得讨论的问题了!
第一种好方法:
在IDEA中有快捷键帮助你完成这个工作,点击你的类,然后右键,选择Generate,然后就可以选择生成一些常用的方法了,其中就有hashcode() 和 equals() 方法!按照这种方法来修改,代码如下:
package SetInterface;
import java.util.*;
/**
* @author jiangzl
*/
public class MyHashSet {
public static void main(String[] args){
HashSet mySet = new HashSet();
mySet.add(null);
mySet.add(123);
mySet.add("hello");
mySet.add(new String("hello"));
mySet.add(new String("hello"));
mySet.add(new Student("jiangzl", 92));
mySet.add(new Student("jiangzl", 92));
Iterator iterator = mySet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
/**
* @author jiangzl
*/
class Student{
private String name;
private int score;
public Student(String name, int score){
this.name = name;
this.score = score;
}
@Override
public String toString(){
return new String("姓名:" + name + " , 分数:" + score);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Student)) {
return false;
}
Student student = (Student) o;
return score == student.score && name.equals(student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, score);
}
}
看看这种方法的结果如何:
可以发现完成了去重!
第而种好方法:
如果你压根不用IDEA这种好的IDE呢?如果你工作的环境没有这么优秀的Generate工具呢?如何书写属于你自己的,相对好使的hashcode() 方法呢?
生成 hashcode() 的第一步:
定义一个初始值,一般设为 : res = 17
生成 hashcode() 的第二步:
根据你的成员变量,逐步生成各自的哈希值:
如果是 Boolean 类型的变量 var1 :[hashcode_var1] = var1 ? 1 : 0
如果是 int 类型(或者size小于int)的变量 var2 :[hashcode_var2] = (int)var2
如果是 long 类型的变量 var3 :[hashcode_var3] = (int) (var3 ^var3 >>>32)
如果是 double 类型的变量 var4 : [hashcode_var4] = var4.hashcode()
如果是某个对象的引用 var5 :[hashcode_var5] = var5.hashcode()
如果是数组类型,则类比String类的hashcode() 方法,计算一个以素数 p = 31(或者131、13131等)为基数的多项式
生成 hashcode() 的第三步:
生成最终的哈希值,也就是对每一个成员变量,依次、递推地计算:
res = res * 31 + [hashcode_varX] 即可!
最后,本例中我的示例代码(重写hashcode()与equals())如下:
package SetInterface;
import java.util.*;
/**
* @author jiangzl
*/
public class MyHashSet {
public static void main(String[] args){
HashSet mySet = new HashSet();
mySet.add(null);
mySet.add(123);
mySet.add("hello");
mySet.add(new String("hello"));
mySet.add(new String("hello"));
mySet.add(new Student("jiangzl", 92));
mySet.add(new Student("jiangzl", 92));
Iterator iterator = mySet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
/**
* @author jiangzl
*/
class Student{
private String name;
private int score;
public String getName() {
return name;
}
public int getScore(){
return score;
}
public void setName(String name) {
this.name = name;
}
public Student(String name, int score){
this.name = name;
this.score = score;
}
@Override
public String toString(){
return new String("姓名:" + name + " , 分数:" + score);
}
@Override
public boolean equals(Object o) {
if(this == o){
return true;
}
if(null == o || this.getClass() != o.getClass()){
return false;
}
Student temp = (Student)o;
if(temp.getScore() == this.getScore()){
if(temp.getName().hashCode() == this.getName().hashCode()){
return temp.getName().equals(this.getName());
}
}
return false;
}
@Override
public int hashCode() {
int res = 17;
res = 31 * res + (int)score;
res = 31 * res + name.hashCode();
return res;
}
}
结果展示如下:
可以看到,效果还是ok的!
四、 TreeSet 集合的使用方法
4.1、看一个简单的例子
package SetInterface;
import java.util.*;
public class MyTreeSet {
public static void main(String[] args){
TreeSet myTreeSet = new TreeSet();
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
for (int i = 0;i < n;++i){
int var = sc.nextInt();
myTreeSet.add(var);
}
Iterator iterator = myTreeSet.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
看看它的输出结果:
从中可以看到,我们在插入元素到TreeSet集合的过程中就已经完成了排序!这也体现了它的底层是二叉排序树,而且是从小到大排序的。那么也就是说,对于一颗二叉树而言,它的递归定义是:左子树小于根节点小于右子树。
并且需要注意的是: 无论是HashSet还是TreeSet对集合的元素进行访问只能用迭代器进行操作,不能用get(index)之类的方法。原因就是:它们在底层实现上是无序的(也就是地址是不连续的),所以不存在有下标index的随机访问,只能用迭代器进行访问!
4.2、比较好用的一些方法
再看看这个例子:
package SetInterface;
import java.util.*;
public class MyTreeSet {
public static void main(String[] args){
TreeSet myTreeSet = new TreeSet();
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
for (int i = 0;i < n;++i){
int var = sc.nextInt();
myTreeSet.add(var);
}
System.out.println(myTreeSet.subSet(3, 7));
System.out.println(myTreeSet.headSet(6));
System.out.println(myTreeSet.tailSet(6));
System.out.println(myTreeSet.tailSet(11));
}
}
看看输出的结果:
说实话哈,一种python的既视感!也就是说,如果我们要使用这些个方法在实际开发或者算法解体中,切记要注意格式的变化,要先去掉中括号,然后按照 “逗号” 进行分割哈!
4.3、关于TreeSet的排序
TreeSet的排序还是老样子,有自然排序和自定义排序。这个就不赘述了,只要是jdk中有的数据类型,都可以自然排序,我们就记录《自定义排序》吧!
如何实现TreeSet的自定义排序?
在TreeSet对象的构造方法中,有这么些方法可以定义排序比较规则:
public TreeSet(Comparator<? super E> var1) {
this((NavigableMap)(new TreeMap(var1)));
}
public TreeSet(SortedSet<E> var1) {
this(var1.comparator());
this.addAll(var1);
}
上面的代码来自jdk1.8的源代码,从此我们可以看到,我们只需要一个 实现 了Comparetor的对象,去给TreeSet对象进行实例化, 就完成了自定义排序的设计
看一个实现自定义排序的例子
package SetInterface;
import java.util.*;
/**
* @author jiangzl
*/
public class MyTreeSet {
public static void main(String[] args){
TreeSet myTreeSet = new TreeSet(new MyComparetor());
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
for (int i = 0;i < n;++i){
String name = sc.next();
int prio = sc.nextInt();
myTreeSet.add(new TreeSetTask(name, prio));
}
Iterator iterator = myTreeSet.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
/**
* @author jiangzl
*/
class TreeSetTask{
private String taskName;
private int taskPriority;
public TreeSetTask(String name, int prio){
this.taskName = name;
this.taskPriority = prio;
}
public String getTaskName(){
return taskName;
}
public int getTaskPriority(){
return taskPriority;
}
@Override
public String toString(){
return new String("任务名:" + taskName + " , 任务优先级:" + taskPriority);
}
@Override
public boolean equals(Object o) {
if (this == o){
return true;
}
if (!(o instanceof TreeSetTask)){
return false;
}
TreeSetTask arraytask = (TreeSetTask) o;
return getTaskPriority() == arraytask.getTaskPriority() && getTaskName().equals(arraytask.getTaskName());
}
@Override
public int hashCode() {
return Objects.hash(getTaskName(), getTaskPriority());
}
}
/**
* @author jiangzl
*/
class MyComparetor implements Comparator{
@Override
public int compare(Object o1, Object o2) {
if((o1 instanceof TreeSetTask) && (o2 instanceof TreeSetTask)){
TreeSetTask task1 = (TreeSetTask) o1;
TreeSetTask task2 = (TreeSetTask) o2;
if(task1.getTaskPriority() != task2.getTaskPriority()){
return task2.getTaskPriority() - task1.getTaskPriority();
}
else{
return task1.getTaskName().compareTo(task2.getTaskName());
}
}
return 0;
}
}
看看输出的效果先:
然后这里需要注意的是:中文编码里,字符串比较用compareTo()方法计算出来的,和ASCii编码计算出来的是相反的!这个也是有点奇怪的,好像昨天实验的过程中,没发现这个问题嗷!!这个问题,有比较懂的大哥还请评论或者私信一下,谢谢啦!
最后的一点点总结!!
要注意哈,HashSet 集合里头是可以插入不同类型的对象的!!我是这么理解的哈,因为HashSet是根据哈希值来index到对象的,所以只需要知晓对象的地址就可以进行存取。但是TreeSet 是需要排序的,如果是不同类型的对象,那压根无法排序,所以就会不支持嗷!