什么是抽象类呢? 嗷就是这个类它很抽象,结束!(bushi
当一个类没有足够的信息来描绘它的对象时,也就是不能实例化出具体的对象时,我们称之为抽象类。
拿我们前面用过很多遍的Animal类来举例,我们能实例化出一个Animal对象吗? 动物是个很抽象的概念,它可以是猫,可以是狗,可以是鸟等等,所以并不能实例化出一个动物对象。同样,我们在Animal类中实现了 eat() 方法,不同的动物有各自不同的 eat() 方法, 那么,我们的Animal类的 eat() 方法就没有具体实现了,因为它只能在具体的子类中才能有实现。所以我们把他定义为抽象方法,不具体实现,这个类也成了抽象类。
//将Animal实现为抽象类
abstract class Animal {
String name;
int age;
public abstract void eat();
}
class Cat extends Animal {
String hair;
public void eat() {
System.out.println(this.name + "吃猫粮!");
}
}
class Dog extends Animal {
String breed;
public void eat() {
System.out.println(this.name + "吃狗粮!");
}
}
如上面代码所示,被 abstract
修饰的类被称为抽象类, 类中被abstract
修饰的成员方法称为成员方法被称为抽象方法。
//抽象类
abstract class Animal {
//成员变量
String name;
int age;
//抽象方法
public abstract void eat();
//普通方法
public void setName(String name) {
this.name = name;
}
}
1. 抽象类不能直接实例化对象,必须被继承,且子类必须重写父类的抽象方法,否则,子类也只能是抽象类,也要用abstract
修饰。
Animal animal = new Animal();
//编译出错
按住 Alt
+ Enter
重写eat方法,就可以了~
这样我们就可以实例化Cat对象了!
final
,static
,private
修饰,因为抽象方法必须在子类中被重写,它的使命就是被重写,被这些修饰符修饰的方法不能在子类中重写。当抽象方法不加访问限定符时,它默认时 public 修饰的
我们知道普通类也可以被继承,普通方法也可以被重写进而发生多态,那为什么还需要抽象类呢?
使用抽象类相当于多了一重编译器校验。
因为抽象类是不能实例化对象的,当我们不小心实例化了抽象的父类的对象,编译器会报错,而如果我们实例化的是普通类父类,编译器不会报错!
类似于final
修饰的变量,不允许用户去修改,在不小心修改时编译器帮我们来报错提醒。 所以,使用抽象类可以预防出错
。
什么是接口? 我们听过USB接口吧,可以插U盘,鼠标,键盘等等,只要插头符合USB协议,就都能使用USB接口。
所以,接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口可以看成是:多个类的公共规范,也是一种引用数据类型。
接口的语法跟类定义类似,只需要把class
换成interface
即可。
我们来用IDEA创建一个接口:
这次选择Interface,输入接口名称,回车!
public interface 接口名称 {
void method();
//这两种是一样的,更推荐上面这种,更加简洁
public abstract void method();
}
Tips:
1.创建接口时, 接口的命名一般以大写字母 “I ”开头。
2.接口的命名一般使用 “形容词” 词性的单词。比如我们常见的Comparable。
public abstract
修饰的,但如果你改成其他的,比如private
就会报错。同时因为是默认public abstract
修饰的,所以方法不能有具体实现。//报错! 不能用private修饰
private interface IMyInterface {
}
public interface IMyInterface {
void method() {
}
//编译器报错,抽象方法不能有具体实现
}
public interface IMyInterface {
void method();
public abstract void method2();
}
public class Test {
public static void main(String[] args) {
IMyInterface myInterface = new IMyInterface();
//报错,不能实例化接口对象
}
}
正确使用方法是:定义一个类来实现接口,使用implement
关键字
class 类名 implements 接口名 {
//在类中必须重写接口的所有方法
}
现在编译器在报错,那是因为没有重写接口方法,我们现在按下Alt
+Enter
,选择 implement method 实现方法,回车。
选择所有的方法,点击OK, 完成!
public interface IMyInterface {
void method();
static void method();
//报错,静态方法不能被重写
IMyInterface() {
}
//报错,不能有构造方法
}
public
修饰。
分析一下,为什么? 还记得我们在多态里说重写的时候吗?我们说重写方法的访问权限必须大于等于父类的方法,既然接口里的方法都是默认public
修饰的,那么重写方法只能是public
修饰了。
那有人就要说了,那不是子类重写父类方法的规则吗?还能用到接口吗?
我们发现在编译后,接口也会生成.class
文件,这跟类是类似的,而实现类implements
实现接口也类似于子类extends
继承父类,虽然不是一个东西不能相提并论,但还是有相似点的,咱们对照理解。
public static final
修饰。即接口中的变量是不能被修改的,得看作常量。在Java中,类和类之间是单继承的,一个类只能继承一个父类,不能多继承。但是接口与接口之间却可以做到多继承,一个接口可以继承多个接口,一个类也能实现多个接口。
下面我们同样通过Animal类来理解多接口和多继承。
因为我们说动物类不能具体实例化,他的eat方法在每个子类的实现不同,所以我们把他写成抽象方法,自然Animal类也就成了抽象类。
abstract class Animal {
//成员变量
String name;
int age;
//抽象方法
public abstract void eat();
//普通方法
private void setName(String name) {
this.name = name;
}
}
然后,提供几个接口,我们知道动物有“会跑的”,“会飞的”,“会游泳的”,所以我们分别提供这三个接口,让具有这种特性的子类来实现它们。
public interface IRunning {
void run();
}
public interface IFlying {
void fly();
}
public interface ISwimming {
void swim();
}
猫猫会跑,鱼会游泳,鸟会飞也会跑,鸭子既能跑又会游泳。我们说猫,鱼,鸟,鸭子都继承于Animal,但是,猫具有会跑的特性,鱼具有会游泳的特性,鸟具有会跑和会飞两种特性,鸭子具有会跑和会游泳两种特性。
类和类之间我们说继承,类和接口直接我们说类具有接口的特性或者类实现了接口的功能。
class Cat extends Animal implements IRunning {
//重写接口方法
@Override
public void run() {
System.out.println(this.name + "正在朝你奔来!");
}
//重写父类方法
@Override
public void eat() {
System.out.println(this.name + " 喵喵!");
}
}
class Fish extends Animal implements ISwimming {
//重写接口方法
@Override
public void swim() {
System.out.println(this.name + "正在游向你!");
}
//重写父类方法
@Override
public void eat() {
System.out.println(this.name + "在吃虾米!");
}
}
class Bird extends Animal implements IRunning, IFlying {
//重写接口方法
@Override
public void fly() {
System.out.println(this.name + "正在扑棱扑棱!");
}
//重写接口方法
@Override
public void run() {
System.out.println(this.name + "正在蹦蹦跳跳跑!");
}
//重写父类方法
@Override
public void eat() {
System.out.println("早起的鸟儿有虫吃!");
}
}
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的。
接口可以继承一个接口, 达到复用的效果. 使用extends
关键字。
举个例子,两栖类动物既会游泳又会跑,我们实现一个两栖类(Amphibious)的接口,这个接口需要同时具备Swimming和Running的特性,所以它继承这两个接口。
interface IAmphibious extends ISwimming, IRunning {
void climb();
}
注意接口继承是不需要重写接口里的方法的,都在实现接口的类里进行重写。接口继承就是缝合怪,把多个接口拼接组装在一起,方便使用。
如果我们相对一个整形数组排序,有很多方法,什么冒泡排序选择排序,在Java中对基本数据类型排序我们有Arrays.sort()
方法,如图:
我们知道基本数据类型的数组都是通过比较大小来排序的,那如果,对一个引用类的数组,数组里存的是对象,该这么排序呢?还能不能比较大小?
class Student {
private String name;
private int age;
public Student(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;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student("zhangsan", 19);
Student student2 = new Student("lisi", 18);
Student student3 = new Student("wangwu", 20);
Student[] students = {student1, student2, student3};
Arrays.sort(students);
//报错!!
System.out.println(Arrays.toString(students));
}
}
我们的解决办法是,让Student类实现Comparable
接口,并重写它的compareTo()
方法,通过调用compareTo来比较两个对象。
这里Comparable接口后面的
是泛型标志,后面介绍,这里理解为占位符,谁实现它就传谁的类型,这里我们写Student,并重写compareTo方法。
这里compareTo方法中只返回了0,我们还需要根据需求添加,因为compareTo方法如果调用者大于传入的参数对象,则返回大于0的数,相等返回0,小于则返回小于0的数。
如果当前对象应排在参数对象之前, 返回小于 0 的数字;
如果当前对象应排在参数对象之后, 返回大于 0 的数字;
如果当前对象和参数对象不分先后, 返回 0;
假如我们通过年龄来比较:
@Override
public int compareTo(Student o) {
return this.age - o.age;
}
students数组按年龄排好序了,有个疑问,我们只是写了compareTo方法,为什么就能排序了呢?
对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力,通过重写 compareTo 方法的方式, 就可以定义比较规则,在 sort 方法中会自动调用 compareTo 方法,通过比较大小来排序。参考冒泡选择排序。
不过,这样写其实不太好,因为,这样写的话,我以后调用sort只能按照年龄大小来排序了,如果我们想按照姓名来排序,或者按照成绩来排序该怎么办??? 所以这样写的缺点是:对类的侵入性太大,不建议这样写!
好的写法是,自己另外实现比较器类,实现Comparator接口,重写compare方法。在sort时调用两个参数的需要传比较器的sort方法!
//年龄比较器
class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
}
注意我在这里通过年龄比较,name是String类型,调用了String的compareTo方法。String的compareTo方法如图,了解即可:
我们要调用的sort方法如图:
public class Test {
public static void main(String[] args) {
Student student1 = new Student("zhangsan", 19);
Student student2 = new Student("lisi", 18);
Student student3 = new Student("wangwu", 20);
Student[] students = {student1, student2, student3};
System.out.println("排序前");
System.out.println(Arrays.toString(students));
//实例化比较器对象
AgeComparator ageComparator = new AgeComparator();
//调用需要比较器的sort方法
Arrays.sort(students, ageComparator);
System.out.println("排序后");
System.out.println(Arrays.toString(students));
}
}
这样,我们就不用在需要不同排序规则时对Student类中的compareTo方法改来改去,降低了对类的侵略性! 例如现在如果Student类中新加属性score成绩,我们需要按成绩排序,只需要实现类,类中实现Comparator接口,按成绩来比较即可。
equals我们在字符串中经常使用,用它来判断两个字符串内容是否相同。如图:
这个equals是String类中自带的,源码如下:
而要用equals来判断两个对象属性是否相同,能直接用吗?来试试。
我们的两个对象内容完全一样,却打印了false,为什么?坏了?我们来看看这个equals,点进去!
原因是这里的equals是继承自Object类(所有类的父类)的,Student本身没有这个方法,它返回的是直接判断两个引用是否相等,即判断地址是否相等,两个对象地址当然不同,永远都是false。
如果要用equals,就需要重写equals方法。
我们鼠标点击右键,找到万能的Generate
,选择equals and hashcode
,回车,然后一路next,就好了。
现在比较就可以了:
这两个都能用来比较两个对象,但是equals用来判断两个对象内容或者属性是否都相等,而compareTo是通过对象的某个属性进行比较,可以判断是否相等,也可以判断大小。
类似点:
- 编译后都会生成
.class
字节码文件- 都能将子类对象通过向上转型由抽象类引用或者接口引用。
- 都需要具体类来重写方法。
核心区别:
抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法。
码字不易,点个赞再走吧,收藏不迷路,持续更新中~