Set接口概述:
1)Set接口是Collection的子接口,set接口没有提供额外的方法。
2)Set集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set集合中,则添加操作失败。
3)Set判断两个对象是否相同不是==运算符,而是根据equals()方法。
Set实现类:HashSet
1)HashSet是Set接口的典型实现,大多数时候使用Set集合时都使用这个实现类。
2)HashSet按Hash算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
3)HashSet具有以下特点:不能保证元素的排列顺序,HashSet不是线程安全,集合元素可以是null。
4)HashSet集合判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的equals()方法返回值也相等。
5)对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
Set实现类:LinkedHashSet
1)LinkedHashSet是HashSet的子类。
2)LinkedHashSet根据元素的hashCode值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
3)LinkedHashSet插入性能略低于HashSet,但在迭代访问Set里的全部元素时有很好的性能。
4)LinkedHashSet不允许集合元素重复。
Set实现类:TreeSet
1)TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素出于排序状态。
2)TreeSet底层使用红黑树结构存储数据。
3)新增的方法如下:Comparator comparator()、Object first()、Object last()、Object lower(Object e)、Object higher(Object e)、SortedSet subSet(fromElement,toElement)、SortedSet headSet(toElement)、SortedSet tailSet(fromElement)
4)TreeSet两种排序方法:自然排序和定制排序。默认情况下,TreeSet采用自然排序。
Set接口的框架:
Collection接口:单列集合,用来存储一个一个的对象
Set接口:存储无序的、不可重复的数据据 --->高中讲的“集合”
HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值。
LinkedHashSet:作为HashSet的子类;遍历内部数据时,可以按照添加顺序遍历。
对于频繁的遍历的操作,LinkedHashSet效率高于HashSet。
TreeSet:可以按照添加对象的指定属性,进行排序。
1)Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。
2)要求:向Set中添加的数据,其所在的类一定要重写hashCode()和equals()。
重写的hashCode()和equals()尽可能保持一致性。
Set的无序性和不可重复性:
以HashSet为例说明:
1)无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据哈希值值决定
2)不可重复性:保证添加的元素按照equals()判断时,不能返回true,即相同的元素只能添加一个。
添加元素的过程:
以HashSet为例说明:
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的的哈希值,此哈希值接着通过某种算法计算处在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:
1)如果此位置上没有其他元素,则元素a添加成功。
2)如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
a)如果hash值不相同,则元素a添加成功;
b)如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,元素a添加失败;
equals()返回false,则元素a添加成功。
对于添加成功的情况而言:元素a与已经存在指定索引位置上数据以链表的方式存储。
jdk7:元素a放到数组中,指向原来的元素。
jdk8:原来的元素在数组中,指向元素a
总结:七上八下
HashSet底层:数组+链表的结构
向HashSet中添加元素的过程:
1)当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值,通过某种散列函数决定该2对象在HashSet底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下表,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)
2)如果两个元素的hashCode()值相等,会再继续调用equals方法,如果过equals方法结果为true,添加失败;如果为false,那么会保存该元素,但是该数组的位置已经有元素了,那么会通过链表的方式继续链接。
3)如果两个元素的equals()方法返回true,但它们的hashCode()返回值不相等,hashSet将会把他们存储再不同的位置,但依然可以添加成功。
Eclipse/IDEA工具里hashCode()重写:
以Eclipse/IDEA为例,在自定义中可以调用工具重写equals和hashCode。问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?
1)选择系数的时候要选择尽量大的系数,因为如果计算出来的hash地址越大,所谓的“冲突“就越少,查找起来效率也会越高。(减少冲突)
2)并且31只占用5bits,相乘造成数据溢出的概率较小。
3)可以由i*31==(i<<5)-1来表示,现在很多虚拟机里面都有做相关的优化。(提高算法效率)
4)31是一个素数,素数作用就是如果用一个数字来乘以这个素数,那么最终出来的结果只能被素数,那么最终出来的结果只能被素数本身和被素数本身和被乘数还有1来整数。(减少冲突)
重写equals()方法的基本原则:
以自定义的Customer类为例,何时需要重写equals()?
1)当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同不同的实例有可能在逻辑上是相等的,但是根据Object.hashCode()方法,他们仅仅是两个对象。
2)因此,违反了“相等的对现象必须具有相等的散列码”。
3)结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行性计算。
重写hashCode()方法的基本原则:
1)在程序运行时,同一个对象多次调用hashCode()方法应该返回相同的值。
2)当两个对象的equals()方法比较返回true时,这连哥哥对象的hashCode()方法的返回值也应相等。
3)对象中用作equals()方法比较Field,都应该用来计算hashCode值。
重写两个方法的小技巧:对象中用作equals()放大比较的Field,都应该用来计算hashCode值。
import org.junit.Test;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SetTest {
@Test
public void test1(){
Set set=new HashSet();
set.add(456);
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new User("Tom",12));
set.add(new User("Tom",12));
set.add(129);
Iterator iterator=set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
//未重写hashCode时User不算重复
/*输出:AA
CC
129
456
123
*/
}
}
}
public class User {
private String name;
private int age;
public User(){
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public User(String name, int age) {
this.name = name;
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;
if (age != user.age) return false;
return name != null ? name.equals(user.name) : user.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
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;
}
}
LinkedHashSet的使用:
LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。
优点:对于频繁的遍历的操作,LinkedHashSet效率高于HashSet
@Test
public void test2(){
Set set=new LinkedHashSet();
set.add(456);
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new User("Tom",12));
set.add(new User("Tom",12));
set.add(129);
Iterator iterator=set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
//未重写hashCode时User不算重复
/*
456
123
AA
CC
User{name='Tom', age=12}
129
*/
}
}
}
TreeSet的使用:
1)向TreeSet中添加的数据,要求是由相同类的对象。
2)两种排序方式:自然排序(实现Comparable接口)和定制排序(Comparator)。
自然排序中,比较两个对象是否相同的标准为compareTo()返回0,不再是equals()。
定制排序中,比较两个对象是否相同的标准为:compare()返回0,不再是equals()
3)TreeSet和TreeMap采用红黑树的存储结构。
特点:有序,查询速度比List快。
//自然排序
@Test
public void test1(){
TreeSet set=new TreeSet();
//失败:不能添加不同类的对象:
//set.add(123);
//set.add(456);
//set.add("AA");
//set.add(new User("Tom",12));
//举例一
//set.add(34);
//set.add(-34);
//set.add(43);
//set.add(11);
//set.add(8);
//-34、8、11、34、43
//举例二
set.add(new User("Tom",12));
set.add(new User("Jerry",12));
set.add(new User("Jim",12));
set.add(new User("Mike",12));
set.add(new User("Jack",12));
set.add(new User("Jack",21));
Iterator iterator=set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
//输出:User{name='Jack', age=12}
//User{name='Jerry', age=12}
//User{name='Jim', age=12}
//User{name='Mike', age=12}
//User{name='Tom', age=12}
// User{name='Jack', age=21}
}
}
//定制排序
@Test
public void test2(){
Comparator com=new Comparator() {
//按照年龄从小到大排序
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1=(User) o1;
User u2=(User) o2;
return Integer.compare(u1.getAge(),u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set=new TreeSet(com);
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",62));
set.add(new User("Jack",33));
set.add(new User("Merry",33));
set.add(new User("Jack",21));
Iterator iterator=set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
//User{name='Jim', age=2}
//User{name='Tom', age=12}
//User{name='Jack', age=21}
//User{name='Jerry', age=32}
//User{name='Jack', age=33}
//User{name='Mike', age=62}
}
}
public class User implements Comparable{
private String name;
private int 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;
}
public User(){
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public User(String name, int age) {
this.name = name;
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;
if (age != user.age) return false;
return name != null ? name.equals(user.name) : user.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
//按照姓名从大到小排序,年龄从小到大排序
@Override
public int compareTo(Object o) {
if(o instanceof User){
User user=(User)o;
//return this.name.compareTo(user.name);从小到大
//return -this.name.compareTo(user.name); 从大到小
int compare=-this.name.compareTo(user.name);
if(compare!=0){
return compare;
}else{
return Integer.compare(this.age,user.age);
}
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
}
练习题:
eg1:定义一个Employee类包含:private成员变量name,age,bitthday,其中birthday为MyDate类的对象:并为每一个属性定义getter,setter方法;并重写toString方法输出name,age,birthday。
MyDate类包含:private成员变量year,month,day;并为每一个属性定义getter,setter方法;
创建该类的5个对象,并把这些对象放入TreeSet集合中,分别按以下两种方式对集合元素进行排序,并遍历输出:
1)使Employee实现Comparable接口,并按name排序
2)创建TreeSet时传入Comparator对象,按生日日期的先后排序
public class MyDate implements Comparable{
private int year;
private int month;
private int day;
public MyDate() {
}
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
@Override
public int compareTo(Object o) {
if(o instanceof MyDate){
MyDate m=(MyDate) o;
//比较年
int minusYear=this.getYear()-m.getYear();
if(minusYear!=0){
return minusYear;
}
//比较月
int minusMonth=this.getMonth()-m.getMonth();
if(minusMonth!=0){
return minusMonth;
}
//比较日
return this.getDay()-m.getDay();
}
throw new RuntimeException("传入的数据类型不一致!");
}
}
public class Employee implements Comparable{
private String name;
private int age;
private MyDate birthday;
public Employee() {
}
public Employee(String name, int age, MyDate birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}
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;
}
public MyDate getBirthday() {
return birthday;
}
public void setBirthday(MyDate birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
", birthday=" + birthday +
'}';
}
@Override
public int compareTo(Object o) {
if(o instanceof Employee){
Employee e=(Employee) o;
return this.name.compareTo(e.name);
}
//return 0;
throw new RuntimeException("传入的数据类型不一致!");
}
}
import org.junit.Test;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
public class EmployeeTest {
//问题一:使用自然排序
@Test
public void test1(){
TreeSet set=new TreeSet();
Employee e1=new Employee("刘德华",55,new MyDate(1965,5,4));
Employee e2=new Employee("张学友",55,new MyDate(1987,5,4));
Employee e3=new Employee("郭富城",55,new MyDate(1987,5,9));
Employee e4=new Employee("黎明",55,new MyDate(1954,8,12));
Employee e5=new Employee("梁朝伟",55,new MyDate(1978,12,4));
set.add(e1);
set.add(e2);
set.add(e3);
set.add(e4);
set.add(e5);
Iterator iterator=set.iterator();
while(iterator.hasNext()){
System.out.println();
}
}
//问题二:按生日日期的先后排序
@Test
public void test2(){
TreeSet set=new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Employee && o2 instanceof Employee){
Employee e1=(Employee) o1;
Employee e2=(Employee) o2;
MyDate b1=e1.getBirthday();
MyDate b2=e2.getBirthday();
//方式一:
//比较年
//int minusYear=b1.getYear()-b2.getYear();
//if(minusYear!=0){
// return minusYear;
//}
//比较月
//int minusMonth=b1.getMonth()-b2.getMonth();
//if(minusMonth!=0){
// return minusMonth;
//}
//比较日
//return b1.getDay()-b2.getDay();
//方式二
return b1.compareTo(b2);
}
//return 0;
throw new RuntimeException("传入的数据类型不一致!");
}
});
Employee e1=new Employee("刘德华",55,new MyDate(1965,5,4));
Employee e2=new Employee("张学友",55,new MyDate(1987,5,4));
Employee e3=new Employee("郭富城",55,new MyDate(1987,5,9));
Employee e4=new Employee("黎明",55,new MyDate(1954,8,12));
Employee e5=new Employee("梁朝伟",55,new MyDate(1978,12,4));
set.add(e1);
set.add(e2);
set.add(e3);
set.add(e4);
set.add(e5);
Iterator iterator=set.iterator();
while(iterator.hasNext()){
System.out.println();
}
}
}
eg2:在List内去除重复数字值,要求尽量简单。
import java.util.List;
public static List duplicateList(List list){
HashSet set=new HashSet();//不重复
set.addAll(list);
return new ArrayList(set);
}
public static void main(String[] args){
List list=new ArrayList();
list.add(new Integer(1));
list.add(new Integer(2));
list.add(new Integer(2));
list.add(new Integer(4));
list.add(new Integer(4));
List list2=duplicateList(list);
for(Object integer:list2){
System.out.println(integer);//1、2、4
}
}
eg3:考虑输出的问题,其中Person类中重写了hashCode()和equal()方法。
public class Person {
private int id;
public String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person() {
}
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
if (id != person.id) return false;
return name != null ? name.equals(person.name) : person.name == null;
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
}
public class MyTest {
@Test
public void test3(){
HashSet set=new HashSet();
Person p1=new Person(1001,"AA");
Person p2=new Person(1002,"BB");
set.add(p1);
set.add(p2);
p1.name="CC";
set.remove(p1);
System.out.println(set);
//[Person{id=1002,name='BB'},Person{id=1001,name='CC'}]
set.add(new Person(1001,"CC"));
System.out.println(set);
//[Person{id=1002,name='BB'},Person{id=1001,name='CC'},Person{id=1001,name='CC'}]
set.add(new Person(1001,"AA"));
System.out.println(set);
//[Person{id=1002,name='BB'},Person{id=1001,name='CC'},Person{id=1001,name='CC'},Person{id=1001,name='AA'}]
}
}