[关闭]
@w460461339 2016-11-16T07:03:48.000000Z 字数 5994 阅读 735

Java学习Day6(HashSet TreeSet)

Java基础


今天学习的主要是Java下Set的几个子类及其特点。不过在那,先看看一个登录注册的实际案例。

1、登录注册案例

此处输入图片的描述

具体的要求见上图,这里不贴源码,只是想说明下, 怎么运用面向对象的方法对该问题进行分析。
运用面向对象的方法,我们需要考虑这么几个问题:

A:有哪些类呢?
B:每个类有哪些东西?
C:类与类之间有什么关系?
1.1有哪些类

一般是这样,我们在写出所需要的类之后,还需要对它进行测试(当然你在自己所创建的类中写main方法测试也是可以的, 但是这样今后还要删除,比较麻烦),所以一般都会大致上分为两个类:

1 实际所需类
2 测试类

至于在实际所需类别中如何进行划分,又怎么分出若干类以及子类,却又另说了。因此,我们在这个例子中,先大致划分为两类:

1 用户类
2 测试类
1.2每个类有哪些东西

在大致区分出类别之后,我们可以开始划分每个类中都包含什么东西。
由之前画的图可以看到,我们的用户类需要包含这么几个东西
成员变量:用户名,密码
成员方法:登陆,注册;获取用户名,密码;设置用户名,密码

但这样的设置有一个问题,以后操作进行添加时,需要在用户类中进行修改,没有达到对象,数据,操作三者分离的目标。因此,我们将针对用户的操作提取出来,将用户类分为:用户基本信息类,用户操作类。在用户基本信息类中,提供用户的用户名和密码,以及相应的设置和获取方法;在用户操作类中,我们接口和相应的子类实现,提供注册,登录的方法。

同时,需要在用户操作类中,提供一个静态变量,以储存所有的用户信息

另外,在测试类中,我们只要在mian方法中将上述内容进行测试即可。

1.3类与类之间的关系

在这里,关系很简单,用户基本信息类提供了用户操作类所需要的对象;用户操作类中储存了用户基本信息类的对象,并对外提供针对用户基本信息类的方法;在测试类中,可以调用这些方法对用户信息类的对象进行操作。

或者这么理解,用户基本信息类提供了针对单个用户基本信息类对象的操作,比如设置,获取等(层面是在对象内部);用户操作类提供了对用户基本信息类对象的方法(层面是在对象对象之间),比如登陆,注册等等;而测试类则提供了测试这些方法的区域。

1.4如何分包

分包主要有这么几种方法:

A 功能划分
B 模块划分
C 先按模块划分,再按功能划分

这次,我们按照功能划分,有:

用户基本描述类包
用户操作接口包
用户操作类包(实现用户操作接口)
用户测试类

2.HashSet

HashSet是Set接口的实现类。它可以利用add,put等方法往里添加元素。具体自己查API。它有两点特性,分别为无序性和唯一性。这和数学中集合的定义基本一致。它的无序性值得是,它并不按照你的输入顺序去储存,它内部自有一套存储规范。

2.1HashSet唯一性的实现

HashSet的底层是HashMap< K,P >,而HashMap又是通过链表加数组实现,和数据结构中的图基本是一个意思。

HashSet的add方法是通过HashMap的put方法来实现的。put方法中包含有HashCode以及euqals方法,基本上就是通过比较HashCode,地址值以及equals方法来进行比较。

  1. public V put(K key, V value) {
  2. if (key == null)
  3. return putForNullKey(value);
  4. int hash = hash(key.hashCode());//获取hash值
  5. //散列表冲突处理方法中的分离链接法
  6. int i = indexFor(hash, table.length);//散列表的第一个索引
  7. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  8. Object k;
  9. //先比较Hash值,再比较地址值,最后比较内容
  10. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  11. V oldValue = e.value;
  12. e.value = value;
  13. e.recordAccess(this);
  14. return oldValue;
  15. }
  16. }
  17. modCount++;
  18. addEntry(hash, key, value, i);
  19. return null;
  20. }

其实看的明白的话,这其实的数据结构中的散列表,首先通过不同的hash值将待储存元素挂在不同的类别下(其实是散列表冲突处理方法中的分离链接法)。之后若存在相同hash的元素,只要把这一串上的元素逐一进行比较,就可以判定是否可以把这个元素进行添加,省去了全局进行比较的麻烦。

2.2利用HashSet储存字符串以及自定义对象

储存字符串是没有什么问题的,这个试了就很明了,这里也不写代码了。麻烦的是自定义对象,由于在添加的时候,会自动调用对象的equals以及hashCode方法,而自定义类若没有重写这些方法的话,会自动调用Object的equals以及hashCode方法,最后转化成地址的比较,会使得唯一性特性失效。因此,若想利用集合的唯一性对自定义对象进行储存,就需要在自定义类中重写equals以及hashCode方法。当然,贴心的java以及Eclipse早就帮我们做好了,只要右击添加即可。

  1. public class myHashSet {
  2. private String name;
  3. private int age;
  4. public myHashSet() {
  5. super();
  6. // TODO Auto-generated constructor stub
  7. }
  8. //楼下的get以及set函数都是自动生成的
  9. public String getName() {
  10. return name;
  11. }
  12. public void setName(String name) {
  13. this.name = name;
  14. }
  15. public int getAge() {
  16. return age;
  17. }
  18. public void setAge(int age) {
  19. this.age = age;
  20. }
  21. //这个也都是自动生成的,我们只需要知道原理就好,重写就让他们做
  22. @Override
  23. public int hashCode() {
  24. final int prime = 31;
  25. int result = 1;
  26. result = prime * result + age;
  27. result = prime * result + ((name == null) ? 0 : name.hashCode());
  28. return result;
  29. }
  30. @Override
  31. public boolean equals(Object obj) {
  32. if (this == obj)
  33. return true;
  34. if (obj == null)
  35. return false;
  36. if (getClass() != obj.getClass())
  37. return false;
  38. myHashSet other = (myHashSet) obj;
  39. if (age != other.age)
  40. return false;
  41. if (name == null) {
  42. if (other.name != null)
  43. return false;
  44. } else if (!name.equals(other.name))
  45. return false;
  46. return true;
  47. }
  48. }

3.TreeSet

TreeSet底层是依赖TreeMap实现的(还是实现了接口navigableMap)。具体来说,TreeSet的add方法是利用一个NavigableMap接口的具体实现类来实现的,而这个具体实现类就是TreeMap。数据结构是红黑树。红黑树是AVL树(自平衡二叉树)的变化版。两者是在查找和添加删除元素的时间上有差别,这里不细说,在时间不敏感的程序中意义不大。

TreeSet除了保留了Set的无序性以及唯一性以外,它可以根据用户选择,对所储存的元素按照一定规则进行排序,然后输出。它排序的方法主要有两种,分别是:自然排序比较器排序

3.1自然排序

自然排序即按照其内置的顺序对元素进行排序。若要实现自然排序,在创建TreeSet对象的时候,直接使用默认构造器就可以了。

  1. import java.util.TreeSet;
  2. public class myTreeSet {
  3. public static void main(String[] args) {
  4. TreeSet<String> ts=new TreeSet<String>();
  5. ts.add("hello");
  6. ts.add("world");
  7. ts.add("aloha");
  8. for(String s:ts){//输出:aloha,hello,world
  9. System.out.println(s);
  10. }
  11. }
  12. }

而我们通过查阅TreeMap的put方法可以看到,它的自然排序主要是通过一个名为compareTo的方法来进行的。我们在String类中也找到了这个方法,那么大致可以猜到,TreeMap是调用每个特定类中的compareTo方法来进行比较。而object中并不包含这个方法,所以若不在自定义类中重写这个方法,就会报错

  1. class Student{
  2. private int age;
  3. public Student(int age) {
  4. super();
  5. this.age = age;
  6. }
  7. public int getAge() {
  8. return age;
  9. }
  10. public void setAge(int age) {
  11. this.age = age;
  12. }
  13. }
  14. public class myTreeSet {
  15. public static void main(String[] args) {
  16. TreeSet<Student> ts2=new TreeSet<Student>();
  17. ts2.add(new Student(1));
  18. ts2.add(new Student(2));
  19. ts2.add(new Student(3));
  20. ts2.add(new Student(4));
  21. for(Student s : ts2){//报错,大概就是说,Student不能被转型成为comparable的对象
  22. System.out.println(s.getAge());
  23. }
  24. }
  25. }

解决办法,需要让自定义子类实现Comparable接口,才可以进行比较。而Comparable接口中只有一个方法,就是compareTo方法:

  1. class Student implements Comparable<Student>{//点明是与Student类比较
  2. private int age;
  3. public Student(int age) {
  4. super();
  5. this.age = age;
  6. }
  7. public int getAge() {
  8. return age;
  9. }
  10. public void setAge(int age) {
  11. this.age = age;
  12. }
  13. @Override//实现compareTo方法
  14. public int compareTo(Student o) {
  15. int number=age-o.getage();
  16. return number;
  17. }
  18. }
  19. public class myTreeSet {
  20. public static void main(String[] args) {
  21. TreeSet<Student> ts2=new TreeSet<Student>();
  22. ts2.add(new Student(1));
  23. ts2.add(new Student(2));
  24. ts2.add(new Student(3));
  25. ts2.add(new Student(4));
  26. for(Student s : ts2){//这样就不会报错了。并且按照年龄排序
  27. System.out.println(s.getAge());
  28. }
  29. }
  30. }
3.2比较器排序

之前我们是将我们想要的比较方法定义在我们的自定义类中,在TreeSet想要进行自然排序的时候就调用它。但若是我们想把这个排序方法单独拿出来,让类显得更加的精简,就需要用到比较器排序。

要想使用比较器排序,我们需要在创建TreeSet对象的时候就传入一个Comparator的对象,让TreeSet利用我们传入的Comparator进行排序。

由于Comparator是一个接口,我们需要自己去实现它具体写法:

  1. class MyComparator implements Comparator<Student> {
  2. //表明比较的对象一定是Student的
  3. @Override
  4. public int compare(Student s1, Student s2) {
  5. // int num = this.name.length() - s.name.length();
  6. // this -- s1
  7. // s -- s2
  8. // 姓名长度
  9. int num = s1.getName().length() - s2.getName().length();
  10. // 姓名内容
  11. int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
  12. // 年龄
  13. int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
  14. return num3;
  15. }
  16. }
  17. class Student {
  18. private String name;
  19. private int age;
  20. public Student() {
  21. super();
  22. }
  23. public Student(String name, int age) {
  24. super();
  25. this.name = name;
  26. this.age = age;
  27. }
  28. public String getName() {
  29. return name;
  30. }
  31. public void setName(String name) {
  32. this.name = name;
  33. }
  34. public int getAge() {
  35. return age;
  36. }
  37. public void setAge(int age) {
  38. this.age = age;
  39. }
  40. }
  41. public class myComparator {
  42. TreeSet<Student> ts= new TreeSet<Student>(new MyComparator());
  43. }

当然,若这个比较器只要用一次,可以用匿名内部类的方式实现(匿名内部类主要要去实现一个接口或者抽象类哦):

  1. TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
  2. @Override
  3. public int compare(Student s1, Student s2) {
  4. // 姓名长度
  5. int num = s1.getName().length() - s2.getName().length();
  6. // 姓名内容
  7. int num2 = num == 0 ? s1.getName().compareTo(s2.getName())
  8. : num;
  9. // 年龄
  10. int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
  11. return num3;
  12. }
  13. });

4.LinkedHashSet

这个就是用链表以及HashMap实现的HashSet,它保留了集合的唯一性,但通过加入了链表,使得它变得有序了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注