在之前的博客中讲解到了Collection
接口下的一个子接口List
,现在对Collection
接口的另外一个子接口进行讲解
集合框架(Set
集合概述及特点)
Set
集合概述及特点: 通过API查看得知:Set
就是一个不包含重复元素的Collection
。
集合框架(HashSet
存储字符串并遍历)
HashSet
是Set
接口的实现类,元素唯一,无序(存取顺序不一致)
HashSet
底层数据结构是哈希表:元素为链表的数组,具有链表和数组的特点 像新华字典(JDK1.7)
HashSet
的构造方法:
HashSet()
构造一个新的空set
,其底层HashMap
实例的默认初始容量是 16,加载因子是
0.75。 就是 当集合被使用达到了初始容量的0.75是就会对原列表进行扩充HashSet(Collection<? extends E> c)
构造一个包含指定collection
中的元素的新set
。
代码示例:
package org.westos.java;
import java.util.HashSet;
/**
* @Author: Administrator
* @CreateTime: 2019-05-11 09:11
*/
public class MyTest {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("q");
set.add("d");
set.add("v");
set.add("v");
set.add("t");
set.add("p");
for (String s : set) {
System.out.println(s);
}
}
}
在上述代码中使用HashSet类的空参构造来创建了一个Set对象,在该对象中一次添加一些字符串,运行上述代码,可知运行结果为:
p
q
d
t
v
从上述结果可以看出,集合中的内容是无序的即存取顺序不一致,而且重复的元素也被去除
集合框架(HashSet保证元素唯一性)
HashSet
底层数据结构是哈希表. HashSet
不是线程安全的,但是效率高, 集合元素可以是 null
哈希表:是一个元素为链表的数组,综合了数组和链表的优点 (像新华字典一样) (JDK1.7之前)
HashSet
唯一性及无序性的实现步骤:
-
当向
HashSet
集合中存入一个元素时,HashSet
会调用该对象的hashCode()
方法来得到该对象的
hashCode
值, -
然后根据
hashCode
值决定该元素所处的对象数组中的索引值,确定完成后 -
确定我要放入集合的元素的值是否和该索引处的链表上的各个元素相等,如果不相等,就将该元素链接在表尾;如果相等,就不会链接, 通过这样就确保了集合中的元素的唯一性,
-
也正是因为在存的过程中会通过哈希值来确定在数组位置的索引,所以实现的无序性
HashSet
集合判断两个元素相等的标准:
两个对象通过 hashCode()
方法比较相等,并且两个对象的 equals()
方法返回值也相等。
结论:HashSet
保证元素唯一性是靠元素重写hashCode()
和equals()
方法来保证的,如果不重写则无法保证。
源码查看:
HashSet<Integer> set = new HashSet<>();
set.add(100);
在上述代码中长按Ctrl键将鼠标放在add
方法上可以查看add
方法的实现,可以查看到如下内容:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
之后再次长按Ctrl键并将鼠标放置到put
方法上,可以查看到put方法的源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); //求出了要添加的对象的哈希值,确定了存放在数组的某个区间的位置索引
}
按照上述方法再次查看putVal
方法,可以看到该方法的源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //调用了添加的对象的equal方法,来比较我要添加的元素和索引处链表上的各个元素的值是否相等
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
上述代码调用了要添加的对象的equal方法,自定义类中直接继承Object类的equal方法,但是该方法默认比较的是两个局部变量中所存的地址值,而我们需要对对象中的内容进行比较的话就必须要对该方法进行重写
在上述代码中讲解了为什么要对equal
方法进行重写,下面要讲解为什么对hashCode()
方法进行重写
在下面的代码中:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在该代码块中长按Ctrl键查看hash
方法的源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 该方法调用了要添加的对象的
hashCode()
方法,但是为什么要重写这个方法呢,因为在我们自定义类时,如果我们创建了两个对象,两个对象的成员变量完全相等,但是因为不是相同的对象,所以会有不同的哈希值,但是对用户来说,如果两个对象的成员一样,就会将他们视为同一个对象,就可以将这两个对象中的任意一个对象删除。 - 在上面讲到了
HashSet
集合判断两个元素相等的标准: 两个对象通过hashCode()
方法比较相等,并且两个对象的
equals()
方法返回值也相等, - 根据上面讲到的标准来判断之前提到的两个对象,因为两个对象不一样,所以第一条标准便不满足,这就导致有两个内容完全相等的对象但在计算机判断时时不同的两个对象,所以依然会将重复的元素存入哈希表中,这就不能确保
HashSet
集合中不存在重复元素,所以必须重写hashCode()
方法。 - 而且在判断元素是否相等是,不再比较两个局部变量的地址值,而是比较两个元素的内容,所以需要对
equal
方法进行重写 - 而
Integer
类和String
类已经默认重写了hashCode()
和equals()
方法,所以在上述的代码示例中才能实现去重的功能,如果要存储自定义类的对象,就要进行这两个方法的重写。 - 而合理的重写
hashCode()
方法可以减少碰撞次数,就是在同一条链表上比较各个元素内容是否相等的次数
集合框架(HashSet存储自定义对象保证元素唯一性)
通过上面的讲解,可以基本理解HashSet实现元素唯一性的步骤。看看下面的代码:
package org.westos.java;
import java.util.HashSet;
import java.util.Objects;
/**
* @Author: Administrator
* @CreateTime: 2019-05-11 09:22
*/
public class MyTest {
public static void main(String[] args) {
Student s1 = new Student("王五", 25);
Student s2 = new Student("王五", 25);
Student s3 = new Student("王五", 25);
Student s4 = new Student("王五", 252);
Student s5 = new Student("王五2", 235);
Student s6 = new Student("王五3", 25);
Student s7 = new Student("王五4", 2665);
Student s8 = new Student("王五5", 285);
Student s9 = new Student("王五6", 285);
Student s10 = new Student("王五7", 255);
HashSet<Student> hashSet = new HashSet<>();
hashSet.add(s1);
hashSet.add(s2);
hashSet.add(s3);
hashSet.add(s4);
hashSet.add(s5);
hashSet.add(s6);
hashSet.add(s7);
hashSet.add(s8);
hashSet.add(s9);
hashSet.add(s10);
for (Student student : hashSet) {
System.out.println(student.getName() + "==" + student.getAge());
}
}
}
class Student {
private String name;
private int age;
public Student() {
}
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 +
'}';
}
@Override
public boolean equals(Object o) { //对equal方法的重写在之前的Object类的博客中进行过详细的讲解,读者可以自行查阅
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);
}
}
在上述代码中重写了那两个方法,实现了集合中元素的唯一性,代码运行结果如下:
王五4==2665
王五==25
王五3==25
王五6==285
王五5==285
王五7==255
王五==252
王五2==235
但是代码依然无序,即元素存取顺序不一致
集合框架(HashSet存储自定义对象保证元素唯一性图解及代码优化)
上述代码如果不对hashCode()
方法进行重写,那么每个对象有不同的哈希值,所以也会被存放在不同的索引处,所以无法确保元素唯一;我们对该方法进行简单的重写,让他只返回哈希值0
,这样所有元素的哈希值都一样,就会都被存放在数组的索引0
处,但是也导致要确保元素唯一性就必须让链表末尾的元素和该链表中的每一个元素进行比较,这样也就是之前提到的增加了碰撞次数,所以可以对hashCode()
方法进行合理的重写来减少碰撞次数,这里我们直接使用Java提供的自带的重写后的方法
集合框架(LinkedHashSet
的概述和使用)
底层数据结构有两个: 链表和哈希表
链表保证有序 哈希表保证元素唯一
注意:这里的有序是指元素的存取顺序一致
代码示例:
package org.westos.demo3;
import java.util.LinkedHashSet;
/**
* @Author: Administrator
* @CreateTime: 2019-05-11 10:17
*/
public class MyTest {
public static void main(String[] args) {
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("A");
linkedHashSet.add("B");
linkedHashSet.add("D");
linkedHashSet.add("E");
linkedHashSet.add("C");
linkedHashSet.add("E");
linkedHashSet.add("C");
linkedHashSet.add("E");
linkedHashSet.add("C");
for (String s : linkedHashSet) {
System.out.println(s);
}
}
}
运行程序,可以看到运行结果为:
A
B
D
E
C
这样既能保证元素的唯一性,也能保证元素的存取顺序一致
集合框架(TreeSet
的概述和引用)
TreeSet
集合的特点: 底层数据结构是二叉树,集合中元素唯一,并且可以对元素进行排序
排序:
-
自然排序
-
使用比较器排序
到底使用的是哪一种的排序取决于构造方法
- 空参构造,那么就使用的是自然排序;
- 有参构造,可以使用比较器来排序
代码示例:
package org.westos.java;
import java.util.TreeSet;
/**
* @Author: Administrator
* @CreateTime: 2019-05-11 10:20
*/
public class MyTest {
public static void main(String[] args) {
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(20);
treeSet.add(18);
treeSet.add(23);
treeSet.add(22);
treeSet.add(17);
treeSet.add(24);
treeSet.add(19);
treeSet.add(18);
treeSet.add(24);
for (Integer integer : treeSet) {
System.out.println(integer);
}
}
}
在上述代码中给集合中添加了多个元素,运行结果如下:
17
18
19
20
22
23
24
可知,集合中的元素唯一且进行了排序
注意:使用TreeSet
集合进行元素的自然排序,那么对元素有要求,要求这个元素必须实现Comparable
接口并重写comPareTo
方法,根据此方法的返回值的正负0 来决定元素在二叉树的位置,否则无法进行自然排序,那为什么上述代码没有实现Comparable
接口呢,那是因为Integer
类已经实现了该接口,并且重写了里面的抽象方法,如果是自定义类那么就必须实现该接口
代码示例2:
package org.westos.java;
import java.util.TreeSet;
/**
* @Author: Administrator
* @CreateTime: 2019-05-11 10:31
*/
public class MyTest {
public static void main(String[] args) {
//按照学生的年龄大小来排序
Student s1 = new Student("王五", 21);
Student s11 = new Student("王五", 21);
Student s123 = new Student("王五3333333333", 21);
Student s2 = new Student("王五", 22);
Student s3 = new Student("王五111", 25);
Student s4 = new Student("王五3333", 252);
Student s5 = new Student("王五22222222222", 235);
Student s6 = new Student("王五3", 25);
Student s7 = new Student("王五2222222224", 2665);
Student s8 = new Student("王五5", 288);
Student s9 = new Student("王五22226", 285);
Student s10 = new Student("王五7", 255);
TreeSet<Student> treeSet = new TreeSet<>();
treeSet.add(s1);
treeSet.add(s11);
treeSet.add(s2);
treeSet.add(s3);
treeSet.add(s4);
treeSet.add(s5);
treeSet.add(s6);
treeSet.add(s7);
treeSet.add(s8);
treeSet.add(s9);
treeSet.add(s10);
treeSet.add(s123);
for (Student student : treeSet) {
System.out.println(student);
}
}
}
class Student implements Comparable<Student> {
private String name;
private int age;
public Student() {
}
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 +
'}';
}
@Override
public int compareTo(Student s) {
//比较逻辑是按照年龄大小来排序
int num = this.age - s.age;
//当年龄相同不能说明他是同一个对象,还得比较姓名
int num2 = num == 0 ? this.name.compareTo(s.name) : num;
return num2;
}
}
在上述代码中,我们自定义了Student
类,并且让该类实现了Comparable
接口并重写了compareTo
方法,下面来详细讲解如何重写compareTo
方法
二叉树我们会先将第一个元素设置为根结点,因为我们是按照学生的年龄来对Student
对象进行排序,所以会把第二个元素的年龄与根节点的年龄进行比较,如果比第一个大,就放置在第一个结点的右边,如果比第一个小,就放在左边,如果一样就不储存;存储完成后,后续的元素重复执行上述步骤,直到存储完毕。下面是在 TreeSet
集合中存储Integer
类的数据时的图示:
在上图中,如果要再次存储24
,就会和20
比较,将24
存到20
右边,然后又会用24
和23
这个结点进行比较,24
比23
大,所以就存在23
右边,但是发现23
右边的结点已经存在24
就不会进行存储,保证了集合中元素的唯一性和有序性
但是在上述代码示例中如果要重写compareTo
时候,可能会发生一些特殊情况:
如果两个学生姓名不一样,但是成绩一样,在重写的方法里面只写对成绩进行比较,那么就会将这两个不一样的对象认为是同一个对象,不会存储后面的对象,这是一种用户不想看到的情况,那么如何解决这个问题呢?
我们还需要对姓名进行比较,如果成绩一样而且姓名也一样,就认定两个对象完全一样(但是在现实生活中也会有可能发生这种情况,在这里不再考虑,主要理解概念),不保存后面的对象,这样就不会发生元素缺失的情况,代码实现如下:
@Override
public int compareTo(Student s) {
//比较逻辑是按照年龄大小来排序
int num = this.age - s.age;
//当年龄相同不能说明他是同一个对象,还得比较姓名
int num2 = num == 0 ? this.name.compareTo(s.name) : num;
return num2;
}
compareTo(String anotherString)
:按字典顺序比较两个字符串。如果参数字符串等于此字符串,则返回值 0
;如果此字符串按字典顺序小于字符串参数,则返回一个小于 0
的值;如果此字符串按字典顺序大于字符串参数,则返回一个大于 0
的值。
- 如果
num != 0
,肯定不是同一个对象,可以将后面的元素存入二叉树中 - 如果
num == 0
则执行int num2 = num == 0 ? this.name.compareTo(s.name) : num;
,在num == 0
时,比较后面元素的姓名和现在的元素的姓名,如果相等this.name.compareTo(s.name)
执行后返回1
;否则返回0
,并且赋给num2
:
后面的num
并不会起作用,只是为了让三目运算符的格式完整,因为既然执行这条语句肯定num == 0
是成立的,肯定只执行int num2 = num == 0 ? this.name.compareTo(s.name) : num;
语句,如果后面元素的姓名和现在的元素的姓名相等num2=0
, 否则num2
是正或负
,然后根据返回的num2
的值来判断是不存入二叉树还是存放在结点的右边还是左边- 通过上述的代码便可以实现集合中元素的有序和唯一
- 需要注意的是
TreeSet
集合不允许存null
值
集合框架(TreeSet
存储自定义对象并遍历练习)
代码示例:
package org.westos.java;
import java.util.TreeSet;
/**
* @Author: Administrator
* @CreateTime: 2019-05-11 10:31
*/
public class MyTest {
public static void main(String[] args) {
//按照学生的年龄大小来排序
Student s1 = new Student("王五", 21);
Student s222222 = new Student("李四", 21);
Student s11 = new Student("王五", 22);
Student s123 = new Student("王五3333333333", 21);
Student s2 = new Student("王五", 22);
Student s3 = new Student("王五111", 25);
Student s4 = new Student("王五3333", 252);
Student s110 = new Student("王五3333", 253);
Student s5 = new Student("王五22222222222", 235);
Student s6 = new Student("王五3", 25);
Student s7 = new Student("王五2222222224", 2665);
Student s8 = new Student("王五5", 288);
Student s9 = new Student("王五22226", 285);
Student s10 = new Student("王五7", 255);
//使用的是自然排序:
TreeSet<Student> treeSet = new TreeSet<>();
treeSet.add(s1);
treeSet.add(s11);
treeSet.add(s2);
treeSet.add(s3);
treeSet.add(s4);
treeSet.add(s5);
treeSet.add(s6);
treeSet.add(s7);
treeSet.add(s8);
treeSet.add(s9);
treeSet.add(s10);
treeSet.add(s123);
treeSet.add(s222222);
treeSet.add(s110);
for (Student student : treeSet) {
System.out.println(student);
}
}
}
class Student implements Comparable<Student> {
private String name;
private int age;
public Student() {
}
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 +
'}';
}
@Override
public int compareTo(Student s) {
//比较姓名的长度
int num = this.name.length() - s.name.length();
//姓名长度一样后,还得比较姓名的内容
int num2 = num == 0 ? this.name.compareTo(s.name) : num;
//姓名长度一样,姓名内容一样,还得比较年龄
int num3 = num2 == 0 ? this.age - s.age : num2;
return -num3;
}
}
上述代码实现的需求是:
- 先按照学生姓名的长度进行排序,
- 如果长度一样按姓名字典顺序进行排序,
- 如果上述二者还一样就按照年龄进行排序
上述代码运行结果为:
Student{name='王五22222222222', age=235}
Student{name='王五3333333333', age=21}
Student{name='王五2222222224', age=2665}
Student{name='王五22226', age=285}
Student{name='王五3333', age=253}
Student{name='王五3333', age=252}
Student{name='王五111', age=25}
Student{name='王五7', age=255}
Student{name='王五5', age=288}
Student{name='王五3', age=25}
Student{name='王五', age=22}
Student{name='王五', age=21}
Student{name='李四', age=21}
通过运行结果可知实现了需求
集合框架(TreeSet比较器排序的原理及代码实现)
在创建TreeSet
集合时,如果使用有参构造,传递一个比较器,便可实现排序
代码示例:
package org.westos.java;
import java.util.Comparator;
import java.util.TreeSet;
/**
* @Author: Administrator
* @CreateTime: 2019-05-11 11:15
*/
public class MyTest {
public static void main(String[] args) {
MyComparator myComparator = new MyComparator();
TreeSet<Student> treeSet = new TreeSet<>(myComparator); //TreeSet有参构造
treeSet.add(new Student("王五", 21));
treeSet.add(new Student("王五1", 212));
treeSet.add(new Student("王五2", 241));
treeSet.add(new Student("王五3", 2771));
treeSet.add(new Student("王五4", 251));
treeSet.add(new Student("王五5", 2661));
treeSet.add(new Student("王五6", 2661));
treeSet.add(new Student("王五7", 217));
for (Student student : treeSet) {
System.out.println(student);
}
}
}
class MyComparator implements Comparator<Student> { //自定义类来实现比较器的实例化
@Override
public int compare(Student s1, Student s2) {
//按照年龄来排
int num = s1.getAge() - s2.getAge();
int num2=num==0?s1.getName().compareTo(s2.getName()):num;
return num2;
}
}
class Student {
private String name;
private int age;
public Student() {
}
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 +
'}';
}
}
- 因为比较器是个接口,不能实例化,可以自定义一个类来实现
Comparator
接口,并且重写该接口中的compare
方法,重写的内容和之前讲过的无参构造重写compareTo
方法重写逻辑一样,这样比较器便可以实例化 - 之后创建一个比较器对象,将该比较器对象传入
TreeSet
有参构造来说实现唯一和排序 - 元素在二叉树中的存储方式和之前讲过的无参构造相同
当然,因为比较器是个接口,可以使用匿名内部类来实现,而且可以用到其他类的成员方法中:
代码示例2:
package org.westos.demo8;
import java.util.ArrayList;
import java.util.Comparator;
/**
* @Author: Administrator
* @CreateTime: 2019-05-11 11:27
*/
public class MyTest {
public static void main(String[] args) {
ArrayList<Integer> integers = new ArrayList<>();
integers.add(21000000);
integers.add(2210);
integers.add(2130);
integers.add(2150);
integers.add(2150);
integers.add(21770);
integers.add(21550);
integers.sort(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a-b;
}
});
System.out.println(integers);
}
}
这里让Integer
类来实现了比较器的匿名内部类,实现排序,匿名内部类总的逻辑与上面讲到的相同
代码示例3:
package org.westos.demo8;
import java.util.Arrays;
import java.util.Comparator;
/**
* @Author: Administrator
* @CreateTime: 2019-05-11 11:29
*/
public class MyTest2 {
public static void main(String[] args) {
Integer[] arr = {2, 4, 0, 1, 40, 24};
// Arrays.sort(arr);
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return -(a - b);
}
});
System.out.println(Arrays.toString(arr));
}
}
这里让 Arrays
类的sort
方法实现了比较器接口的匿名内部类,实现排序,内部类内容同上