Java集合框架(三)—Set集合
一、Set接口
Set中存放的是唯一,无序的数据(存入和去重的顺序不一定一致)
操作数据的方法与List类似,Set不可以通过下标获取对应位置的元素的值,因为无序的特点,因此Set接口不存在get()方法
Set接口中的实现类包括:HashSet、TreeSet
Set set = new HashSet(); set.add("123"); set.add(1); set.add(false); set.add("123"); System.out.println(set); 结果: [1, 123, false]
1.1 HashSet
HashSet:采用Hashtable哈希表存储结构
- 优点:添加速度快,查询速度快,删除速度快
- 缺点:无序
- LikedHashSet
-采用哈希表存储结构,同时使用链表维护次序
-有序(添加顺序)
设置元素的时候,如果是自定义对象,会查找对象中的equals和hashCode方法,如果没有,比较的是地址。因此需要注意将这两个方法加上
定义一个Peron实体类,Person类中未添加equals()和hashCode()方法
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; }
// 添加equals和hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
测试类将结果进行打印,可知得出三条结果,重写equals和hashCode方法,在将结果进行打印可得出两条
HashSet hashSet = new HashSet(); hashSet.add(new Person("张三",13)); hashSet.add(new Person("张三",13)); hashSet.add(new Person("李四",12)); System.out.println(hashSet);
结果:
[com.kmm.Person@74a14482, com.kmm.Person@4554617c, com.kmm.Person@1540e19d]
二次结果:
[com.kmm.Person@16e8e05, com.kmm.Person@18e5420]
总结:HashSet是如何保证元素的唯一性的?
是通过元素的两个方法,hashCode和equals方法来完成
如果元素的HashCode值相同,才会判断equals是否为true
如果元素的hashCode值不相同,不会调用equals方法
1.2 TreeSet
TreeSet:底层的实现是TreeMap,利用红黑树来进行实现
- 优点:有序(排序后的升序),查询速度比List快
- 缺点:查询速度没有HashSet快
树中的元素是要默认进行排序操作的,如果是基本数据类型,自动比较,如果是引用类型,需要自定义比较器。
比较器分类:
内部比较器:定义在元素的类中,通过实现Comparable接口来进行实现
外部比较器:定义在当前类中,通过实现Comparator接口来实现,但是要将该比较器传递到集合中
- 基本数据类型的TreeSet实现,满足其有序且排序后升序的特点。默认进行排序操作
TreeSet treeSet = new TreeSet(); treeSet.add(1); treeSet.add(3); treeSet.add(2); treeSet.add(4); System.out.println(treeSet);
结果:
[1, 2, 3, 4]
- 自定义比较器,使用内部比较器实现,在Person类中实现Comparable接口并实现其compareTo()方法。根据name的长度从大到小排序
public class Person implements Comparable { ...... ....... /** * 根据name的长度进行排序 * @param o * @return */ @Override public int compareTo(Object o) { Person person = (Person)o; if (person.name.length()>this.name.length()){ return 1; }else if(person.name.length()<this.name.length()){ return -1; }else { return 0; } } }
测试类部分代码如下:
TreeSet treeSet = new TreeSet(); treeSet.add(new Person("zhangsan",13)); treeSet.add(new Person("zhangsan",13)); treeSet.add(new Person("lisi",12)); treeSet.add(new Person("wangwu",12)); treeSet.add(new Person("maliu",12)); System.out.println(treeSet);
结果:
[Person{name='zhangsan', age=13}, Person{name='wangwu', age=12}, Person{name='maliu', age=12}, Person{name='lisi', age=12}]
若想将结果按name长度从小到大进行排序,只需要将return返回值交换即可。
- 自定义比较器,使用外部比较器,在测试类中实现Comparator接口,实现其compare(Object o1, Object o2)方法。此处一定要注意,要将该比较器传递到集合中
public class TreeSetDemo implements Comparator<Person> { public static void main(String[] args) { TreeSet treeSet = new TreeSet(new TreeSetDemo()); treeSet.add(new Person("zhangsan",13)); treeSet.add(new Person("zhangsan",14)); treeSet.add(new Person("lisi",12)); treeSet.add(new Person("wangwu",10)); treeSet.add(new Person("maliu",15)); System.out.println(treeSet); } @Override public int compare(Person o1, Person o2) { if (o1.getAge()>o2.getAge()){ return 1; }else if(o1.getAge()<o2.getAge()){ return -1; }else { return 0; } } }
注意:
外部比较器可以定义成一个工具类,此时所有需要比较的规则如果一致的话,可以复用,而内部比较器只有在存储当前对象的时候才可以使用。
如果两个比较器同时存在,会默认使用外部比较器。
当使用比较器的时候,不会调用equals方法
二、泛型
当做一些集合的统一操作时,需要保证集合的类型是统一的,此时需要泛型来进行限制。
2.1 特点
1、数据安全
2、获取数据时效率比较高
给集合中的元素设置相同的类型就是泛型的基本需求
使用:
在定义对象的时候,通过<>中设置合理的类型来进行实现
List<String> list = new ArrayList<String>(); list.add("1"); list.add("abc"); list.add("true"); list.add(new Person("zhangsan",12).toString()); System.out.println(list);
2.2 泛型的高阶应用
1、泛型类
当定义类的时候在类名的后面添加<E、V、K、A...>等,起到占位的作用,类中的方法的返回值类型和属性的类型都可以使用
2、泛型接口
当定义接口的时候,在接口的名称后添加<E、V、K、A...>等
1、子类在进行实现的时候,可以不填写泛型的类型,此时在创建具体的子类对象的时候才决定使用什么类型
2、子类在实现泛型接口的时候,只在实现父类的接口的时候指定父类的泛型类型即可,此时,测试方法中的泛型类型必须要跟子类保持一致
3、泛型方法
在定义方法的时候,指定方法的返回值和参数是自定义的占位符,可以是父类名中的T,也可以是自定义的Q,只不过在使用Q的是需要使用<Q>定义在返回值的前面
4、泛型的上限(工作中不用):如果父类确定了,所有的子类都可以直接使用
5、泛型的下限(工作中不用):如果子类确定了,子类所有的父类都可以直接传到参数使用