黑马程序员---java基础之集合Set

------<a href="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------

 

Set集合

今天我们来学习Collection的另一个子接口Set接口,上回我说到Collection体系中的List接口,来复习一下,List的三个实现的子类,ArrayList,Vector,LinkedList。
LinkIterator是List的迭代器,其中hasMoreprevious和nextPrevious是其特有的功能,返回列表中的上一个元素。
ArrayList底层是数组,查询元素快,增删元素慢,线程不安全,效率高
Vector底层是数组,查询元素快,增删元素慢,线程安全,效率低。
LinkedList底层是链表,查询元素慢,增删元素快,线程不安全,效率高。
本章来学习Set集合,但在此之前,我们先来学习另外两个知识点,泛型和增强for。

一、泛型(generic)
概述:是一种把类型明确工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。参数化类型,把类型当做参数一样传递。
泛型格式:<数据类型> 这里的数据类型只能是引用数据类型
泛型的好处:a、把运行时期的问题提前到了编译时期
      b、避免了强制类型转换
       c、优化了程序设计
泛型可以定义在类上,接口上,方法上
定义泛型类:public class  类名<泛型类型1,。。。>{} 注意:在这里的泛型必须是引用数据类型
定义泛型方法:public  <泛型类型>返回值  方法名(泛型类型变量名)
定义泛型接口:public interface 接口名<泛型类型>

泛型通配符<?>:任意类型,如果没有明确,那么Object或任意java的类
?extends   E:向上限定,E及其子类
?super E :向下限定,E及其父类

代码实现:
public class GenericDemo {
public static void main(String[] args) {
//任意通配符
Collection<?> c1 = new ArrayList<Object>();
Collection<?> c2 = new ArrayList<Animal>();
Collection<?> c3 = new ArrayList<Dog>();

//指定泛型
Collection<Object> c4 = new ArrayList<Object>();
Collection<Animal> c5 = new ArrayList<Animal>();
Collection<Dog> c6 = new ArrayList<Dog>();

//<? extends E> 向上限定,只能是E及E的子类
//Collection<? extends Animal> c7 = new ArrayList<Object>();错误的
Collection<? extends Animal> c8 = new ArrayList<Animal>();
Collection<? extends Animal> c9 = new ArrayList<Dog>();

//<? super E> 向上限定,只能是E及E的父类
Collection<? super Animal> c10 = new ArrayList<Object>();
Collection<? super Animal> c11 = new ArrayList<Animal>();
// Collection<? super Animal> c12 = new ArrayList<Dog>();错误的
}
}
解释:以上是简略的代码示例,演示泛型的作用,及泛型的限定。

二、增强For
概述:增强for可以简化数组和集合的遍历。
格式: for(元素数据类型变量 : 数组或者集合){
使用变量即可,该变量就是元素
}
其好处就是可以简化集合或数组的遍历
注意:增强for的目标要判断是否为null

代码演示:
public class ForDemo {
public static void main(String[] args) {
ArrayList<Dog> a = new ArrayList<Dog>();
Dog d1 = new Dog("黄飞鸿",3);
Dog d2 = new Dog("花无缺",4);
a.add(d1);
a.add(d2);

for(Dog d : a){
System.out.println(d.getName()+"---"+d.getAge());
}
}
}
运行结果:
黄飞鸿---3
花无缺---4

三、Set集合
Set集合:元素唯一,并且无序。在添加元素的时候,集合有对元素进行处理的自己的顺序,如果我们添加的时候刚好和这个顺序相同,不能说明其是有序的。
Set是一个接口,继承自Collection接口和Iterable接口,而今天我们主要学习其三个实现的子类,HashSet,LinkedHashSet,TreeSet。但在此之前我们先来了解一下set集合。
集合中的方法可通过查阅API得知,在此我就直接演示了
代码演示:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SetDemo {
public static void main(String[] args) {
Set<String> str = new HashSet<String>();
str.add("hello");
str.add("world");
str.add("java");

Iterator<String> it = str.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
System.out.println("----------------------------");
Set<Student> sten = new HashSet<Student>();
Student s1 = new Student("小明",23);
Student s2 = new Student("小白",24);
Student s3 = new Student("小红",25);
sten.add(s1);
sten.add(s2);
sten.add(s3);
for(Student sn : sten){
System.out.println(sn.getName()+"---"+sn.getAge());
 }
}
}
运行结果:
hello
java
world
----------------------------
小明---23
小白---24
小红---25
解释:Set是接口,不能创建对象,所以用其实现了HashSet多态形式创建对象,其中对集合的操作方法查阅API可知,我用了两种遍历方式,一种是迭代器,一种是增强for。分别向集合中添加字符串和学生对象,进行遍历。不要误会,这两种方法都可以遍历合集,并不是字符串只能用迭代器遍历,而对象只能用增强for遍历。

其中有很多细节问题,比如为什么无序,为什么唯一,这些问题我们放在其实现子类中介绍。

四、HashSet
HsahSet底层是哈希表数据结构,可以保证元素的唯一性,但不保证元素有序
唯一性主要依赖于两个方法:一个是hasCode方法,一个是equals方法。
如果我们想要存储元素,想让元素具有唯一性,只需要在对应的元素中重写hasCode方法和equals方法
代码演示:集合中添加字符串
import java.util.HashSet;
public class HashSetDemo {
public static void main(String[] args) {
HashSet<String> h = new HashSet<String>();
h.add("hello");
h.add("world");
h.add("java");
h.add("world");

for(String s : h){
System.out.println(s);
}
}
}
运行结果:
hello
java
world
解释:上面中我添加了同样的字符串, world “,但是打印的时候只有一个,说明其唯一性,但是为什么唯一。
查看原码可知:在String类中,有equals方法和hashCode方法,在添加的时候就会进行比较,看元素是否相同,是就不添加,不是才添加。下面来看一下原码:
h.add("hello") ;
h.add("world") ;
h.add("java") ;
h.add("world") ;

public interface Collection {....}
public interface Set extends Collection {...}
public class HashSet implements Set {
private static final Object PRESENT = new Object();//成员类
public boolean add(E e) { //e="hello"
        return map.put(e, PRESENT)==null;
    }
//add方法可以看出,其本质是返回map的put方法。
//map就是HashSet的构造方法中的new HashMap<>(),如下所示,在我们new HashSet()的时候就已经创建//了,创建了一个HashMap的对象,将地址值赋给map,这样map就指向了HashMap,代表了HashMap
public HashSet() {
        map = new HashMap<>(); //返回一个hashmap对象
    }

public class HashMap {
//这是HashMap的put方法,首先传递了两个参数key和vaule,hello就key的值
public V put(K key, V value) {// key="hello",vaule是PERSON 也就上面的成员类
// 这一步是判读哈希表是否存在 , 如果不存在就创建一个哈希表
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
// key="hello",所以这一步不走
            return putForNullKey(value);           
        int hash = hash(key);
//将key的值传递给hash方法, 通过hashCode方法计算一个int类型的值
       
        int i = indexFor(hash, table.length);    
 // 在哈希表中查找是否存在该hash值
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//这一步要等有重复元素是才会走
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//比较哈希值是否相等,如果相等
                V oldValue = e.value;//再判断是否是同一对象,或者判断内容是否相等
                e.value = value;
                e.recordAccess(this);
                return oldValue; //如果是同一元素,就返回oldVaule,就是该元素没有被添加进去
            }
        }
        modCount++;
        addEntry(hash, key, value, i); // 把元素添加到集合中
        return null;
    }    
//这是hash方法,用来获取hashCode值
final int hash(Object k) {// key="hello"
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();// key="hello"
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);//最终影响返回结果的就是h ^= k.hashCode();这一步,这是计算哈希值
    }
}
private transient HashMap<E,Object> map;//hashmap集合

}

    transient int hashSeed = 0;
上面的原码是为了说明方便而粘贴下来的
总结:其原理就是将元素的哈希值计算出来,和哈希表中的比较,如果没有哈希表就创建一个,比较之后判断是否存在,由判断是否是同一对象或相等,是就不添加,不是就添加,

HashSet添加对象元素:
import java.util.HashSet;
public class HashSetDemo2 {
public static void main(String[] args) {
HashSet<Student> h = new HashSet<Student>();
Student s1 = new Student("小王",23);
Student s2 = new Student("小张",24);
Student s3 = new Student("小李",21);
Student s4 = new Student("小王",23);
h.add(s1);
h.add(s2);
h.add(s3);
h.add(s4);
for(Student s : h){
System.out.println(s.getName()+"----"+s.getAge());
}
}
}
运行结果:
小王----23
小张----24
小李----21
解释:如果想保证添加对象的唯一性,就要在对象类中重写hashCode和equals方法。
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
不用担心,可以使用eclipse自动生成,但是要知道原理,和上面的其实差不多,都是首先计算哈希值,然后比较哈希值,在比较对象和内容,相同就不添加,不同才添加



五、LinkedHashSet
数据结构:链表和哈希表,有序
有序性:是靠链表保证
唯一性:是靠哈希表保证的
代码示例:
import java.util.LinkedHashSet;
public class LinkedHashSetDemo {
public static void main(String[] args) {
LinkedHashSet<String> hs = new LinkedHashSet<String>() ;
hs.add("hello") ;
hs.add("world") ;
hs.add("java") ;
hs.add("world") ;
for(String s : hs){
System.out.println(s);
}
}
}
运行结果:
hello
world
java



六、TreeSet
数据结构:底层是二叉树
代码实现:
public class TreeSetDemo {
public static void main(String[] args) {
TreeSet<Integer> ts = new TreeSet<Integer>() ;
// 添加元素
// 存储下列元素:  20 , 18 , 23 , 22 , 17 , 24, 19 , 18 , 24
ts.add(20) ;
ts.add(18) ;
ts.add(23) ;
ts.add(22) ;
ts.add(17) ;
ts.add(24) ;
ts.add(19) ;
ts.add(18) ;
ts.add(24) ;
// 遍历
for(Integer i : ts){
System.out.println(i);
}
}
}
运行结果:17,18,19,20,22,23,24
二叉树原理分析图:

TreeSet特点:元素唯一,并且可以元素进行排序。
排序方式:
1、自然排序
2、使用比较器排序
而到底使用哪种排序方法,这取决于构造方法(可通过API查阅,构造方法和应用方法)

自然排序:要求对应的元素必须实现Comparable接口,重写其中的compareTo方法
唯一性:是判断方法的返回值是否为0,如果为0,则认为元素相同,不进行添加
代码实现:
第一种方法
public class Student implements Comparable<Student>{//实现Comparable接口
@Override
public int compareTo(Student o) {//重写compareTo方法
// TODO Auto-generated method stub
int num = this.age - o.age;
int num2 = (num == 0)? this.name.compareTo(o.name) : num ;
return num2;
}
private String name;
private int age;
public Student() {
super();
// TODO Auto-generated constructor stub
}
public Student(String name, int age) {
super();
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;
}
}
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
TreeSet<Student> t = new TreeSet<Student>();
Student s1 = new Student("小张",25);
Student s2 = new Student("小王",23);
Student s3 = new Student("小李",24);
t.add(s1);
t.add(s2);
t.add(s3);
for(Student s : t){
System.out.println(s.getName()+"----"+s.getAge());
}
}
}
运行结果:
小王----23
小李----24
小张----25
解释:学生类实现了Comparable接口,重写了compareTo方法。对对象的年龄进行排序
第二种方法:
/*
 * 存储自定义对象
 * 按照姓名的长度进行排序
主要条件是姓名的长度
然后是姓名
然后是年龄
 * */
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
TreeSet<Student> t = new TreeSet<Student>(new Comparator<Student>(){
@Override
public int compare(Student o1, Student o2) {
// TODO Auto-generated method stub
int num = o1.getName().length() - o2.getName().length();
int num2 = (num == 0)? o1.getName().compareTo(o2.getName()) : num;
int num3 = (num2 == 0)?o1.getAge() - o2.getAge() : num2;
return num3;
}
});
Student s1 = new Student("xiaoming",25);
Student s2 = new Student("xiahong",27);
Student s3 = new Student("xiaozhang",21);
Student s4 = new Student("xiaoli",25);
Student s5 = new Student("xibai",23);
Student s6 = new Student("xiaoli",25);
Student s7 = new Student("xiaoming",25);
Student s8 = new Student("xiaoming",29);
t.add(s1);
t.add(s2);
t.add(s3);
t.add(s4);
t.add(s5);
t.add(s6);
t.add(s7);
t.add(s8);
for(Student s : t){
System.out.println(s.getName()+"----"+s.getAge());
}
}
运行结果:
xibai----23
xiaoli----25
xiahong----27
xiaoming----25
xiaoming----29
xiaozhang----21
解释:完成了需求,首先对名字的长度排序,然后是名字,然后是年龄,用比较器方式。

上面是TreeSet的两种比较方式,虽然有原理图,但可能大家还是不知道为什么TreeSet会进行排序,下面我们来看一下原码,和刚才的HashSet有一些相同之处
public interface Collection {...}//Collection接口
public interface Set extends Collection {...} //Set接口继承了Collection接口
public TreeSet implements Set { //TreeSet类实现了Set接口
private transient NavigableMap<E,Object> m;//成员类

private static final Object PRESENT = new Object();//成员静态类
//方法之一,传递参数是NavigableMap<E,Object> m类型,
TreeSet(NavigableMap<E,Object> m) {//NavigableMap<E,Object> m也是一个接口,TreeMap是其子接口
        this.m = m;
    }
//无参构造方法,创建一个TreeMap对象
public TreeSet() {
        this(new TreeMap<E,Object>());
    }
//有参构造方法,创建一个比较器的TreeMap
public TreeSet(Comparator<? super E> comparator) {//
        this(new TreeMap<>(comparator));
    }
//从这一行开始分析,向和集中添加元素,本质上是调用m.put方法。
public boolean add(E e) {//m实质上是NavigableMap<E,Object> m = new TreeMap()
        return m.put(e, PRESENT)==null;//以动态的形式创建的对象,并调用其方法
    }
}
public class TreeMap {
private final Comparator<? super K> comparator;
//构造方法
public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator; //传递您自己定义的比较器,按照您的比较方式
    }
//构造方法
public TreeMap() {
        comparator = null;
    }
//put方法,向Map中添加key和vaule,其中key实质就是我们要传递的元素,vaule在此处您可以忽略。
public V put(K key, V value) {

        Entry<K,V> t = root; //root代表树根
        
        // 创建树根,如果不存在就创建一个
        if (t == null) {
            compare(key, key); 


            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        
        int cmp;
        Entry<K,V> parent;
        //从此处开始,是最为重点,最为核心的代码,才是比较器的实质
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key); //通过比较方法比较,返回结果
                if (cmp < 0) //如果返回值为负数,则放在树节点的左边
                    t = t.left;
                else if (cmp > 0) //如果返回值为整数,则放在树节点的右边
                    t = t.right;
                else
                    return t.setValue(value); //如果返回时0,则说明相等,不添加此元素
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException(); //如果是空节点,报异常
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
}
解释:首先创建树根节点,然后依次向内添加元素,将添加的元素和节点元素比较,看返回值,按照我们制定的比较规则,判断返回值,如果返回值为负数,则放在节点的左边,如果返回值为正数,则放在节点的右边。依次添加,依次比较,向下发展,直至元素添加完毕。
上面的代码只是大致讲解一下原码的运行,只是对于比较器更深层的代码解析,如果还是不太了解TreeSet的保证元素有序的原理,就先记住其保证有序格式和方法,以后在慢慢研究。














































  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值