接口使用实例(Comparable 接口与Comparator接口)
Comparable接口
刚才的关于例子比较抽象, 我们再来一个更能实际的例子.
给对象数组排序
给定一个学生类
class Student { private String name; private int score; public Student(String name, int score) { this.name = name; this.score = score; } @Override public String toString() { return "[" + this.name + ":" + this.score + "]"; } }
再给定一个学生对象数组
:
Student[]students=new Student[]{ new Student("张三",95), new Student("李四",96), new Student("王五",97), new Student("赵六",92), };
现对这个对象数组中的元素进行排序(按分数降序)
.
按照我们之前的理解, 数组我们有一个现成的 sort 方法, 能否直接使用这个方法呢
?
Arrays.sort(students); System.out.println(Arrays.toString(students)); // 运行出错, 抛出异常. Exception in thread"main"java.lang.ClassCastException:Student cannot be cast to java.lang.Comparable
我们呢会发现此时会发生类型转换异常,仔细思考, 不难发现, 和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确. 而两个学生对象的大小关系怎么确定? 需要我们额外指定.
假设我们不去指定的话,我们也不知道到底是按照学生姓名比较,还是学生的年龄来进行比较.
此时就需要让我们的 Student 类实现 Comparable 接口, 并重写 其中的 抽象的compareTo 方法,下面来看代码:
1.//自定义类型比较大小需要实现Comparable接口,<>为泛型 2.//对于Comparable接口来说,一般都是在类的内部定义的 3.class Student implements Comparable<Student> { 4. public String name; 5. public int age; 6. public int score; 7. 8. public Student(String name, int age, int score) { 9. this.name = name; 10. this.age = age; 11. this.score = score; 12. } 13. 14. //重写toString方法 15. @Override 16. public String toString() { 17. return "Student{" + 18. "name='" + name + '\'' + 19. ", age=" + age + 20. ", score=" + score + 21. '}'; 22. } 23. 24. //因为此时实现了Compareable接口,则需要重写其内部的抽象方法compareTo 25. @Override 26. public int compareTo(Student o) { 27. //通过分数来进行排序,如果想通过年龄等直接修改score替换成age即可 28. //如果大于的时候return 1,小于return -1,说明是按照从小到大的顺序排列的 29. //如果大于的时候return -1,小于return 1,说明是按照从大到小的顺序排列的 30. if(this.score > o.score) { 31. return 1; 32. }else if(this.score == o.score) { 33. return 0; 34. }else { 35. return -1; 36. } 37. } 38.} 39.public class TestDemo2 { 40. 41. public static void main(String[] args) { 42. //如果想要对Student类的引用进行大小比较,就需要Student类去实现Comparable接口 43. Student student1 = new Student("bit",18,79); 44. Student student2 = new Student("gao",29,70); 45. Student student3 = new Student("shasha",17,99); 46. 47. Student[] students = new Student[3]; 48. students[0] = student1; 49. students[1] = student2; 50. students[2] = student3; 51. 52. //sort方法默认从小到打排序 53. Arrays.sort(students); 54. System.out.println(Arrays.toString(students)); 55. } 56.}
此时我们通过分数进行排序,在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象. 然后比较当前对象和参数对象的大小关系(按分数来算).
如果当前对象应排在参数对象之前, 返回小于 0 的数字;
如果当前对象应排在参数对象之后, 返回大于 0 的数字;
如果当前对象和参数对象不分先后, 返回 0;
再次执行程序, 结果就符合预期了.
// 执行结果
[Student{name=‘gao’, age=29, score=70}, Student{name=‘bit’, age=18, score=79}, Student{name=‘shasha’, age=17, score=99}]
注意:
但是上述的比较方式是有局限性的,因为上述的比较方式中是直接把比较写死的,可以说只能比较年龄,不能比较姓名等其他东西,但是我现在就想比较除掉年龄的其他东西该怎么办?此时就用到了比较器.
对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则.
Comparator接口(比较器)
Comparable接口 与Comparator接口的区别
首先我们知道的是Compareable接口是定义在类的内部 来实现对象的比较的,而我们比较器Comparator是专门定义在类的外部的
代码示例
还是之前的学生类,不同的是Student类不再实现Compareable接口:
此时我们想根据分数来对学生进行排序,那就在类的外部重新定义一个public类去实现Comparator接口,重写Comparator接口中的compare方法,以此来实现我们对于分数的比较和排序:来看代码:
首先来看Comparator接口中的compare方法:
来看对于分数比较的代码
//ScoreComparator.java类 public class ScoreComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.score - o2.score; } }
如果还想要对于学生的名字进行比较:来看代码实现:
public class NameComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.name.compareTo(o2.name); } }
这块的代码就不能再使用o1.name-o2.name这样的写法了,原因是我们的name是String类型,是不能进行相加减的,所以此处应该使用compareTo方法,有些同学可能就会纳闷了,compareTo方法出现于Compareable接口中,为什么要在这里使用这个呢?
答案如下:
我们先来看String类的源码:
可以看到String类实现了我们的Compareable接口,并重写了我们Compareable接口中的compareTo方法
所以关于名字的比较器我们可以知道它虽然实现了Comparator接口
,但是其内部使用的compareTo方法来源于Compareable接口
.
下面来看主函数
中是如何实现对于名字和分数的比较的:
class Student { public String name; public int score; public Student(String name, int score) { this.name = name; this.score = score; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", score=" + score + '}'; } } public class TestDemo2 { public static void main(String[] args) { Student[] students = new Student[3]; students[0] = new Student("bit", 89); students[1] = new Student("abc", 19); students[2] = new Student("htf", 59); System.out.println("===============没有排序前=============="); System.out.println(Arrays.toString(students)); System.out.println("===============根据分数排序后=============="); ScoreComparator scoreComparator = new ScoreComparator(); //传两个参数进来 Arrays.sort(students, scoreComparator); //输出结果为:[Student{name='abc', score=19}, Student{name='htf', score=59}, Student{name='bit', score=89}] System.out.println(Arrays.toString(students)); System.out.println("===============根据姓名进行排序后=============="); NameComparator nameComparator = new NameComparator(); Arrays.sort(students, nameComparator); //输出结果为:[Student{name='abc', score=19}, Student{name='bit', score=89}, Student{name='htf', score=59}] //以上姓名的排序是按照首字母进行排序的 System.out.println(Arrays.toString(students)); } }
接口间的继承(扩展)
接口可以继承一个接口,也可以继承(扩展)多个接口
,已达到复用的效果. 使用 extends 关键字.当然这里的继承关系我们把它理解成扩展的意思更为准确
interface IRunning { void run(); } interface ISwimming { void swim(); } // 两栖的动物, 既能跑, 也能游 interface IAmphibious extends IRunning, ISwimming { //此时IAmphibious这个接口相当于有了两个方法,一个是run,一个是swim } class Frog implements IAmphibious { //此时Frog这个类必须重写IAmphibious这个接口的两个抽象方法,一个run方法,一个swim方法 }
通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法, 也需要实现 swim 方法.
总结
抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别(重要!!! 常见面试题).
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法.
如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口.
class Animal { protected String name; public Animal(String name) { this.name = name; } }
再次提醒:
抽象类存在的意义是为了让编译器更好的校验, 像 Animal 这样的类我们并不会直接使用, 而是使用它的子类. 万一不小心创建了Animal 的实例, 编译器会及时提醒我们.