java基础第18天



Set接口概述


1 Set与Collection的关系
  Set是一种Collection,即Set接口是Collection接口的子接口。


2 Set支持泛型
  Java中所有集合类都支持泛型,这一点不在赘述。


3 Set没有添加新方法
  Set中的方法全部是从Collection继承的,自己没有添加任何一个方法。


4 Set的基本特性 
  模仿数学中的“集”。
  不要求顺序,
  无重复元素
  没下标


5 Set的add()方法返回值有意义
  List#add()方法返回值永远是true
  而Set#add()方法返回值可能是false,这一特性是因为Set不包含重复元素。如果一个元素多次 添加到同一Set中,那么就会添加失败,所以返回false。


6 遍历Set只能使用Iterator
  Set没有下标,所以不能像List一样使用下标来遍历。
  Set只能使用Iterator来遍历了!


7 常用实现类
  HashSet、TreeSet、LinkedHashSet
  默认使用HashSet


HashSet


1 HashSet特性
  不同步,即线程不安全的
  无序
  无重复元素(所有Set都是这个特性)
  底层使用哈希表结构


2 使用String测试HashSet
  测试toString()。
  测试Iterator迭代,查看输出顺序。
  测试add()、contains()、remove()方法


3 使用Person(自定义类型)测试HashSet
  测试toString()。
  测试Iterator迭代,查看输出顺序。
  测试add()、contains()、remove()方法,注意这些方法是否能正确运行!
  重写Person类的equals()方法后,再测试add()、contains()、remove()等方法。


4 HashSet是怎么保证元素唯一性的?
先比较hashCode()
再使用equals()比较
  如果hashCode()相等,再使用equals()比较;如果hashCode()不相等,那么就不再使用equals()比较了。
  如果想让你的哈希表听话,需要重写元素类型的两个方法:
  hashCode()和equals()
   如果两个对象的equals()比较为true,那么hashCode()必须相同。反之,没有要求!
  
  例如:Person比较的是两个对象的name、age、sex,
  那么hashCode()使用name、age、sex的hashCode()相加,这就能保存上面的条件了。基本类型就本身也就可以了,要么也可以转换成对应的包装器类型,再去获取hashCode()。
  


哈希表结构


1 桶数组
  哈希表是一个桶数组,也就是说一个哈希表中有多个桶!
  每个桶可以存放多个元素。可以把桶理解为链表(集合)!这样整个哈希表就是拥有多个链表的集合了。


2 计算桶位
  哈希表中有很多桶,即一个桶数组。所谓桶位就是对应桶数组的下标!


3 添加元素的流程
  当把元素添加到哈希表中时,需要先找到元素对应的桶位,然后判断这个桶中是否存在这个元素,如果元素在桶中已经存在,那么添加失败;否则添加成功!
获取元素的哈希码值(使用元素的hashCode()方法);
通过哈希码值计算桶位(可以把哈希码值理解为就是桶位);
遍历桶中元素,使用元素的equals()方法,验证元素是否在桶中已经存在;
存在则添加失败,否则把元素添加到桶中。


4 添加元素的问题
  HashSet<Person> set = new HashSet<Person>();
  Person p1 = new Person(“zhangSan”, 23, “male”);
  Person p2 = new Person(“zhangSan”, 23, “male”);
  set.add(p1);
  set.add(p2);


  上面代码中set.add(p2)的结果会添加成功!也就是说HashSet会认识p1和p2是两个不同对象。如果想让上面代码中set.add(p2)添加失败,我们需要让HashSet认为p1和p2是相等的。
  set.add(p2):调用p2的hashCode()方法获取哈希码,通过哈希码找到桶。如果p1与p2的hashCode()不同,那么p2找到的桶就与p1找到的桶不同。
  循环遍历p2对应桶中所有元素,使用equals()比较,如果没有相同元素,那么添加p2到这个桶中。
  结论:就算p1.equals(p2)结果为true,但p1.hashCode() != p2.hashCode(),那么也是枉然!


5 HashSet保证元素唯一性
  如果两个对象的hashCode()相等,并且使用equals()方法比较返回true,那么这两个对象是相等的。


编写equals()和hashCode()


1 euqals()方法
  我们以Person类为例:public boolean equals(Object o)
如果当前对象与o指向同一实例,那么直接返回true
if(this == o) return true;
如果o不是Person类型,那么直接返回false
boolean b = o instanceof Person;
if(!b) return false;
把o向下转型为Person类型
Person p = (Person)p;
如果当前对象的name与p对象的name不相等,那么返回false
if(!name.equals(p.name)) return false;
如果当前对象的age与p对象的age不相等,那么返回false
if(!age == p.age) return false;
如果当前对象的sex与p对象的sex不相等,那么返回false
if(!sex.equals(p.sex)) return false;
执行到这里,可以肯定两个元素是相等的了,返回true
return true
public boolean equals(Object o) {
if(this == o) {
return true;
}
boolean b = o instanceof Person;
if(!b) {
return false;
}
Person p = (Person)o;
if(!name.equals(p.name)) {
return false;
}
if(age != p.age) {
return false;
}
if(!sex.equals(p.sex)) {
return false;
}
return true;
}


2 hashCode()方法
  我们以Person类为例:int hashCode()
定义int h = 0,最终返回h;
计算所有属性的哈希值;
计算age:数值类型的属性本身作为值:
h += this.age;
计算name:引用类型的属性调用其hashCode()方法:
h += name.hashCode();
计算sex:引用类型的属性调用其hashCode()方法:
h += sex.hashCode();
最后返回h


  对象的hashCode()都在100之内,这对计算桶的分配会过于其中。例如有一万个桶,而元素都放到了0~10之间的桶中,其它桶中没有元素,这就不好了!所以需要修改上面的hashCode()方法,放大多个对象哈希码之间的间隔。
  int h = 1;//如果为0,那么乘以基数也是枉然!
  int prime = 31;//基数
  h += prime * h + age;
  h += prime * h + name.hashCode();
  h += prime * h + sex.hashCode();
  return h;
public int hashCode() {
int h = 1;
int prime = 31;
h += prime * h + age;
h += prime * h + name.hashCode();
h += prime * h + sex.hashCode();
return h;
}


LinkedHashSet


1 LinkedHashSet是HashSet的子类
  LinkedHashSet是HashSet的子类,也就是说LinkedHashSet底层也使用的是哈希表!


2 LinkedHashSet迭代有序
  LinkedHashSet的迭代顺序是添加顺序!因为内部使用了链表来记录元素的添加顺序,迭代时再使用添加时顺序迭代!


3 LinkedHashSet没有添加新的方法
  LinkedHashSet没有添加新的方法,也就是说,它使用起来与使用HashSet一样,只是它有可预知的顺序,而HashSet是杂乱无章的。


TreeSet
  TreeSet是有序的,它会把元素排序,但如果元素没有自然顺序,那就出错。
  要求元素类型,必须去实现Comparable接口。这个接口只有一个方法:
  compareTo(T o),用来比较当前对象与参数对象谁大谁小。this > o,返回一个正数;this < 0,返回一个负数;否则返回0。
  TreeSet用compareTo()方法保证元素的唯一性,如果新添加的元素,与当前Set中的元素使用compareTo比较结果为0,那么就说明元素已经存在,再添加就重复了,所以添加失败!
1 TreeSet特性
  不同步,即线程不安全。
  有序
  无重复元素(所有Set都是这个特性)
  底层使用二叉树结构


2 使用String元素测试TreeSet
  测试迭代顺序!


3 使用Person元素测试TreeSet
  测试add()方法


4 TreeSet是怎么保证元素唯一性的?
  要求元素具有可比性!
  例如:数值类型都具有可比性:10 > 3
  例如:String类型具有可比性:”abc”.compareTo(“def”) < 0
  
  但是,Person没有可比性。
  任何实现了Comparable接口的类都是具有可比性的!
  
  当两个元素使用compareTo()方法比较返回0时,表示两个元素是相同的!


5 让Person实现Comparable接口
  实现Comparable接口只需要重写一个方法:int compareTo(Person p)方法。
  重写这个方法需要指向出两个Person对象如果比较,是比较姓名,还是比较年龄,或者所有属性都参加比较。
public int compareTo(Person p) {
int n = name.compareTo(p.name);
if(n != 0) {
return n;
}
n = age - p.age;
if(n != 0) {
return n;
}
return sex.compareTo(p.sex);
}
  
  上面代码先是比较两个对象的name,如果name可以分出大小,那么就返回结果;
  如果name分不出大小,那么再去比较年龄,如果年龄分出大小,那么就返回结果;
  如果年龄再分出不结果,那么最终以性别来比较。


6 不负责任的compareTo()方法会让TreeSet出问题
  因为TreeSet是使用compareTo()方法来保证元素唯一性的,也就是说,在添加元素时,如果新添加的元素与TreeSet中任意一个元素使用compareTo()方法比较返回0时,那么添加就会失败!
  例如,让Person的compareTo()方法永远返回0,这说明任何两个元素比较的结果都是0,表示相等。这就会使TreeSet中最多有一个元素。
  例如,让Person的compareTo()方法使用年龄比较,而不使用name和sex,那么只要年龄相等的元素就会被TreeSet视为相等,那么你会发现,在TreeSet中不可能存在两个年龄相等的Person对象。


二叉树


1 什么是二叉树
  二叉树也是一种树状结构。
  二叉树中每个节点最多有两个子节点。
  二叉树中每个节点都比自己左边节点的值大,比右边节点的值小。


2 添加元素
添加进来的第一个元素就是根节点;
然后再添加第二个元素时,与根节点进行比较,如果比根节点大,那么放到根节点的右侧子节点位置;如果比根节点小,那么放到根节点左侧子节点位置;如果与根节点相等,那么添加失败。
添加第三个元素时,先与根比较:
如果比根小,那么再与根的左侧子节点进行比较:
大于左侧节点,新元素放到左节点的右节点位置;
小于左节点,放到左节点的左节点位置;
等于左节点,那么添加失败。
如果比根大,那么再与根的右侧子节点进行比较:
大于右侧节点,新元素放到右节点的右节点位置;
小于左节点,放到左节点的左节点位置;
等于左节点,那么添加失败。
如果等于根节点,那么添加失败。


  例如:向TreeSet中添加Integer,分别是:2,5,8,3,6,9,3
  第一步:添加2到树中,因为没有元素,那么2为根节点:
           


  第二步:添加5到树中,因为5大于根节点2,所以把5放到2的右边:
           


  第三步:添加8到树中,因为8大于根节点2,所以再让8与5比较,因为8大于5,所以把8放到5的右边:
  
           


  第四步:添加3到树中,因为3大于根节点2,所以再让3与5比较,因为3小于5,所以把3放到5的左边:


           


  第五步:添加6到树中,因为6大于根节点2,所以再让6与5比较,因为6大于6,所以让6与8比较,因为6小于8,所以把6放到8的左边:


           


  第六步:添加9到树中,因为9大于根节点2,所以再让9与5比较,因为9大于5,所以让9与8比较,因为9小于8,所以把9放到8的右边:


            


  第七步:添加3到树中,因为3大于根节点2,所以再让3与5比较,因为3小于5,所以让3与3比较,因为3与3相等,所以添加失败!
             


3 遍历二叉树
  遍历二叉树是原则是先左,然后当前节点,最后是右。
  例如还是以上面二叉树结构举例说明:
2是根节点,那么需要再遍历2的左节点,但因为2没有左节点,所以打印;
然后是2的右节点了,即5;
因为5有左节点,所以遍历到3节点;
因为3没有左结节,所打印3,然后轮到3的右节点,但因为3没有右节点,所以打印3结节结束;
5的左节点打印结束,开始打印5本身,然后是打印5右侧节点8;
因为8有左节点,所先打印左节点6;
因为6没有左节点,所打印6本身,因为6没有右节点,所以打印6节点结束;
8节点的左侧打印完毕,开始打印8本身,然后开始打印8的右侧节点9;
因为9没有左节点,所以打印9本身,因为9没有右节点,所以打印9结束;
打印8结束;
打印5结束;
打印2结束。


比较器Comparator


如果TreeSet有比较器,那么就使用比较器来比较元素;
如果TreeSet没有比较器,那么使用元素类型的自然顺序;
如果元素类型没有自然顺序,那么就出异常。


1 当TreeSet元素没有实现Comparable接口
  如果Person没有实现Comparable接口,而且还想把Person对象添加到TreeSet中,这就需要使用给TreeSet添加比较器对象,即Comparable接口。


2 TreeSet排序的两种选择
  要么让元素自己拥有可比较性,即自然顺序;
  要么让TreeSet本身拥有比较器。
  
  TreeSet类的构造器:TreeSet(Comparator c)


3 Comparator接口
  这个接口有两个方法:
boolean equals(Object o):这个方法可以不去理睬它,因为Object类也有这个方法;
int compare(T t1, T t2):比较参数1与参数2谁大谁小,t1>t2返回正数;t1<t2返回负数;否则返回0。


4 谁的优先级高
  当元素拥有自然顺序,并且TreeSet同时也拥有比较器,那么最终使用什么进行比较呢?答案是使用比较器比较。
  也就是说,如果TreeSet拥有比较器,那么使用比较器进行比较。
  这说明修改比较逻辑不用去修改Person类的compareTo()方法,而只需给TreeSet一个新的比较器就可以了。


5 使用匿名内部类给TreeSet指定比较器




Arrays类


ArrayS类常用方法概述
本类所有方法都是静态的!本类方法是针对数组的操作!
void sort(type[], int fromIndex, int toIndex)
int binarySearch(type[], int fromIndex, int toIndex, type key)
String toString()


2 sort()和binarySearch()方法与自然顺序和比较器
  当给引用类型排序,或者在引用类型数组中查找时,需要数组元素拥有自然顺序,或者给方法指定比较器。


Collections


Collections常用方法概述
本类所有方法都是静态的,本类方法都是针对集合的操作。
T max(Collection):求最大元素,依赖元素类型的自然顺序;
T min(Collection):求最小元素;
void sort(Collection):给集合中元素排序,依赖元素类型的自然顺序;
int binarySearch(List, T):在List中查找T,返回其下标。
void reverse(List):把List中所有元素位置反转;
Comparator reverseOrder(Comparator):返回比较器;
Collection synchronizedCollection(Collection):把一个不同步的Collection转换成同步;
List synchronizedList(List):把一个不同步的List转换成一个同步的List;
Set synchronizedSet(Set):把一个不同步的Set转换成一个同步的Set;
Map synchronizedMap(Map):把一个同步的Map转换成一个同步的Map。


2 Collections和Collection的区别
  Collectoins:类、所有方法都是静态的,是集合的工具类,而不是集合类;
  Collection:接口,是集合的根类,是List、Set的父接口。


增强for循环


1 增强for循环概念
  可以循环遍历数组或者集合类。


2 增强for循环的语法格式
  for(元素类型 e : 数组或集合对象) {
  }
  增强for每循环一次,都会把数组或集合中的一个元素赋值给e,从头开始遍历,直到最后一个元素。


3 增强for的优缺点
  只能从头到尾的遍历数组或集合,而不能只遍历部分。
  在遍历List或数组时,不能获取当前元素下标。
  增强for使用便简单。
  增强for比使用迭代器方便一点!


4 增强for与Iterable接口
  任何实现了Iterable接口的类,都可以使用增强for来遍历。


静态导入


1 什么是静态导入
  静态导入也需要使用import关键字;
  静态导入后,在调用静态方法,以及使用静态属性时就可以不再给出类名了,例如向控制台打印时可以把System.out.println()写成out.println();System.exit(0)写成exit(0)。


2 静态导入的语法格式
  import static 包名.类名.静态方法名;
  import static 包名.类名.静态属性名;
  import static 包名.类名.*;


3 静态导入真是鸡肋啊
  不建议使用!
  使用静态导入,使代码可读性降低!


可变参数


1 使用数组为方法参数 
  int sum(int a, int b) {return a + b;}
  int sum(int a, int b, int c) {return a + b;}
  int sum(int a, int b, int c, int d) {return a + b + c + d;}


  看上面代码。我们知道这种重载是无止境的!
  当函数的参数可以是0~n个时,我们最好的办法就是使用数组来处理,例如把上面代码修改为一个函数:
  int sum(int[] arr) {
      int sum = 0;
      for(int i = 0; i < arr.length; i++) {
          sum +=arr[i];
    }
    return sum;
  }
  
  修改后的sum()方法可以计算0~N个整数的和,但调用sum()需要传递一个数组,这使调用这个函数很不方便。
  int arr = {1,2,3,,4,5,5};
  sum(arr);


2 可变参数方法的定义
  可以把数组类型的参数定义为可变参数,例如:
  int sum(int[] arr) {
      int sum = 0;
      for(int i = 0; i < arr.length; i++) {
          sum +=arr[i];
    }
    return sum;
  }
  int sum(int… arr) {
      int sum = 0;
      for(int i = 0; i < arr.length; i++) {
          sum +=arr[i];
    }
    return sum;
  }
  
  上面代码把int[] arr修改为int… arr,其中arr就变成了可变参数。
  可变参数其实就是数组。


3 调用可变参数方法
  当调用int sum(int…arr)方法时就方便多了。如下方式的调用都是正确的:
int[] arr = {1,2,3}; sum(arr);,使用数组调用;
sum();,不给参数调用,表示传递0个元素的数组,即:int[] arr={}; sum(arr);
sum(5);,用一个参数调用,表示传递1个元素的数组,即:int[] arr={5}; sum(arr);
sum(2,3,4,5);,用多个参数调用,表示传递多个元素的数组,即:int[] arr={2,3,4,5}; sum(arr);。


  调用可变参数方法,可以传递0~N个参数来调用,也可以直接传递数组来调用。


4 可变参数方法的要求
一个方法最多只能有一个可变参数;
可变参数必须是最后一个参数;
可变参数只能出现在方法的形参中,局部变量或属性是不能使用这种东西的。


















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值