【自顶向下看Java——深度剖析抽象类和接口】
系列文章目录
????????欢迎大家订阅《计算机底层原理》、《自顶向下看Java》专栏、能够帮助到大家就是对我最大的鼓励、我会持续为大家输出优质内容,敬请期待!
前言
? ? ? ? 这篇文章将为大家重点讲解有关Java当中的抽象类和接口的知识,全方位无死角带你彻底掌握抽象类和接口当中的重要知识点。
一、抽象类
什么是抽象类?
? ? ? ? 在Java当中,抽象类(Abstract Class)是一种不能够实例化的类、他的目的是为了被其他的类继承而设计的,抽象类可以包含抽象方法也可以包含普通方法。
? ? ? ? 抽象方法是一种没有实现体的方法,它只有方法签名而没有方法体、继承这个抽象类的子类必须实现这个抽象类当中的所有抽象方法,如果不实现的话,那么就意味着这个子类继承来父类的抽象方法,而没有实现这些抽象方法,就说明这个子类也是抽象类,那么子类必须也使用abstract修饰。
? ? ? ? 定义抽象类的关键字是abstract定义抽象方法的关键字也是abstract,一个类如果包含抽象方法那么这个类必须声明为抽象类。
package Yangon; abstract public class Person { private int age; abstract public void Print(); } class Student extends Person{ @Override public void Print() { System.out.println("Hello World!"); } } class Main{ public static void main(String[] args) { Student student = new Student(); student.Print(); Person person = new Student(); person.Print(); } }
? ? ? ? 抽象类是不可以被实例化的,只能通过它的子类对象当中的重写方法来进行调用。在面向对象的概念当中所有的对象都需要通过类来描述,但是并不是所有的类都是用来描述对象的,有些类仅仅为了提供一种使用规范,这个类当中并没有足够的信息来描述这个类吗,就比如我定义了一个抽象类Animal,这个类当中定义了很多的抽象方,例如 eat() 方法、sleep()方法,所有的动物对象都会吃饭并且睡觉,这些方法我全部定义成为抽象方法,不提供具体实现,因为不同的动物虽然都会进行这些动作,但是不同的动物再进行这些动作的时候,状态是不一样的,例如🐎是站着睡觉的,而🐕就是躺着睡觉的,所以Animal抽象类当中的抽象方法是不可以提供具体实现的,只能够被具体的子类继承并且重写这些抽象方法的时候才可以实现这些方法。
?abstract class Animal{ abstract public void eat(); abstract public void sleep(); } class Dog extends Animal{ @Override public void eat() { System.out.println("吃狗粮!"); } @Override public void sleep() { System.out.println("躺着睡!"); } } class Horse extends Animal{ @Override public void eat() { System.out.println("吃牧草!"); } @Override public void sleep() { System.out.println("站着睡!"); } }
?为什么要使用抽象类?
? ? ? ??我们从代码的效果来看的话,继承是完全可以解决这些问题的,那么为什么要使用抽象类呢?继承确实是一种实现代码重用和多态性的方式,但并不一定非要使用抽象类、抽象类和继承是相关的概念,但他们有不同的目的和用途。
? ? ? ? 1.目的不同:抽象类的目的是为了作为其他类的基类,提供一个通用的模板,抽象类可以包含抽象方法,这些方法需要在子类中被具体实现,通过继承抽象类,子类必须实现这些抽象方法,通过强制子类实现从而确保子类具有一定的行为,抽象类还可以包含具体方法,这些方法可以在子类当中直接继承或者重写。
? ? ? ? 2.机制不同:继承是代码重用的一种机制,但是它不一定需要使用抽象类,你可以使用普通的类继承来获得代码重用的好处,抽象类的主要优势是它可以包含抽象方法,而这些方法在子类当中必须被实现,从而强制执行一定的约定。
? ? ? ? 我们在选择使用抽象类还是普通类或者是我们一会要提到的接口,取决于我们的具体需求,如果我们希望提供一个通用的模板、并且要求子类必须实现特定的方法,那么抽象类是一个合适的选择,如果我们更关注行为的规范而不是具体的实现,接口可能是更好的选择(接口我稍后会为大家具体讲解),但是如果我们想要使用普通类来继承实现代码重用的话一定要注意,继承普通类的确是一种有效的代码重用方式,但是要注意避免深度继承和过度耦合。
抽象类的访问限定符有什么意义?? ? ? ? 1.public:抽象类可以被其他任何类访问,对的是任何类。
? ? ? ? 2.protected:只能被子类和同一个包内的所有类访问。
? ? ? ? 3.default:这个时候只能被同一包内的类访问。
? ? ? ? 4.private:抽象类不可以使用private来修饰,因为抽象类的目的就是为了被继承,但是private是不可以被继承的,所以这与抽象类的设计初心相违背,所以不允许。
? ? ? ? 那么抽象方法使用访问限定符的规则与普通方法完全一样,这里就不做特别的声明了。注意一点抽象方法也不可以被声明为private,因为这个方法也是必须被子类继承并且实现的。
抽象类和普通类有什么区别?? ? ? ? 抽象类和普通类在Java当中的实现是非常类似的,他们都会被编译成为字节码并且在Java虚拟机当中执行,以下我为大家列举出一些相似点和区别点。
? ? ? ? 相似点:
? ? ? ? 1.字节码生成:抽象类和普通类都会生成字节码文件、其中包含了类的结构、字段、方法等信息。
? ? ? ? 2.内存管理:对象的内存分配和释放由Java虚拟机进行管理,这对于抽象类和普通类都是一样的,他们都会存储在JVM的方法区当中。
? ? ? ? 3.方法调用:对象的方法调用采用虚方法调用的方式,确保在运行时根据对象的实际类型进行动态绑定来实现多态(关于动态绑定和多态的知识点我在上一篇文章已经详细地介绍过了,如果大家需要可以去看我之前的文章,这里就不做赘述了)。
? ? ? ? 区别点:
? ? ? ? 1.实例化:抽象类不能实例化,但是普通类可以直接实例化,抽象类需要通过子类实现并且实例化。
? ? ? ? 2.构造器:抽象类也可以有构造器、用于初始化抽象类的实例、普通类也有构造器、但是抽象类的构造器不能够直接实例化抽象类,而是由子类的构造器来进行调用。
? ? ? ? 3.抽象方法:抽象类可以包含抽象方法,这是一种声明但是不实现的方法,普通类可以包含抽象方法,但是不需要。
? ? ? ? 从底层的视角来看的话,抽象类其实和普通类没有太大的区别,Java编译器和JVM在处理抽象类和普通类时并没有太大的区别,它们都会被翻译成为相应的字节码,然后在虚拟机上执行。抽象类主要提供了一种面向对象的设计机制,强制要求子类实现抽象方法,而相比于抽象类普通类会更加地灵活,可以直接实例化!
拓展:为什么抽象方法不可以使用static来修饰
?????????首先我先为大家讲解一下抽象方法和静态方法的区别:
? ? ? ? ? 静态方法使用static关键字来进行修饰,在编译时会同类的结构信息一同加载到方法区当中,所以静态方法的调用是依赖于类的,属于类级别而不依赖于类的实例,同样静态方法是不可以访问类的实例成员和变量只能访问类的静态成员和变量,而静态方法可以被继承,但是它不会被子类重写,如果子类定义了与父类同名的静态方法,子类当中的静态方法不会覆盖父类当中的静态方法,最重要的一点就是静态方法是不具备多态性的,他在编译时就可以确定方法调用的目标。
? ? ? ? 但是反观抽象类,抽象类被abstract修饰,在抽象的父类当中声明并且必须在子类当中提供实现,然后通过子类的对象实例调用,可以访问父类当中的实例成员和变量,抽象方法和普通方法一样都具有多态性,可以被继承可以被重写,通过向上转型就可以实现多态。
? ? ? ? 其实我说到了这里,大家应该也已经明白了,为什么抽象方法不能够被static修饰,因为抽象方法和静态方法在设计理念上就是相违背的,这里还要多提一句抽象方法也不能够被final修饰,因为抽象方法必须被子类重写。
?二、接口
? ? ? ? Java中接口是一种抽象数据类型,用于定义一组抽象方法、但是不提供方法的具体实现,接口允许类实现这些抽象方法,并在类中提供实现。
1.定义接口:
????????使用interface关键字来定义接口,接口当中包括抽象方法、常量,要注意这里的常量被隐式地指定为public static final,以及默认方法和静态方法。
? ? ? ? 抽象方法:首先说到抽象方法,接口当中的抽象方法就是没有方法体的方法,只有方法签名,实现接口的类必须提供这些方法的具体实现。这里要专门提一句,接口当中的抽象方法会被隐式地修饰成为public abstract,所以接口当中的方法定义不需要很复杂。
? ? ? ? 变量(常量):在接口当中声明的所有变量都是常量,都会被编译器隐式地修饰成为public static final类型,因为接口就是需要被实现的,所以使用public,这些接口当中的变量(实际上是常量)必须被初始化,并且在类当中不可以被修改。
? ? ? ? 默认方法:从Java8 开始引入,默认方法是在接口中包含方法体的方法、他们允许在接口当中添加新的方法,而不会破坏实现这个接口的类。
? ? ? ? 静态方法:静态方法是在接口当中使用static关键字定义的方法。
interface IPrint{ void Print(); default void Func(){ System.out.println("Hello default Func()"); } int I_Value = 10; static void StaticFunc(){ System.out.println("Hello static Func()"); } } class Student2 implements IPrint{ @Override public void Print() { System.out.println("Hello Print()"); } @Override public void Func() { //IPrint.super.Func(); 调用接口当中原本的默认方法 System.out.println("我重写了父类的默认方法!"); } public static void main(String[] args) { IPrint iPrint = new Student2(); iPrint.Func(); iPrint.Print(); System.out.println(I_Value); } }
拓展:
? ? ? ? 很多的小伙伴可能第一次听说接口的默认方法的概念,那么什么是默认方法呢?默认方法的引入是为了在接口当中添加新的方法,而不会影响这个接口的已有类,也就是说,如果我定义了一个接口,并且已经使用类对他进行实现,但是这个时候我发现这个接口并不完善,想要在这个接口当中补充一些新的方法,可是如果我们继续定义抽象方法的话,那就意味着凡是实现过这个接口的类都必须实现这个新增的方法,也就是牵一发而动全身,非常的麻烦,所以也就引入了默认方法,在Java8之前,一旦一个接口当中定义了新的方法,所有实现该接口的类都必须提供该方法的实现,否则就会编译报错。
? ? ? ? 默认方法通过在接口当中直接提供方法体,使得接口可以包含具体的方法实现,这样,实现接口的类如果没有显式提供对默认方法的实现,那么就会使用接口当中的默认实现,这位接口的演进提供了一种向后兼容的方式。
? ? ? ? 在实现类当中,如果希望使用默认方法提供的实现,可以直接继承接口当中的默认方法,如果需要覆盖默认方法,可以在实现类中重新实现该方法。
? ? ? ? 这种设计的好处在于,当接口需要添加新的方法的时候,不会破坏已有的实现类,因为它们可以选择是否提供新的方法,而不会强制所有类都做出修改,这有助于在不破坏现有代码的情况下,为接口添加新的功能,增强接口的灵活性。
interface IWork{ void eat(); int I_Value = 10; default void print(){ System.out.println("接口当中的常量:" + I_Value); } } public class Animal { private String name; } class Dog extends Animal implements IWork{ @Override public void eat() { } @Override public void print() { IWork.super.print(); } }
? ? ? ? 如上面的代码所示,接口当中定义了默认方法,如果类当中没有明确重写这个接口当中的默认方法,那么默认还使用接口当中原本定义的默认方法,大家可以运行一下看看效果。
这里要提示一下大家,可能会有小伙伴认为既然是默认方法,那么这个default访问权限不写也可以,他还是默认方法,这是绝对不可以的这里的default关键字必须加上,这是Java8之后独有的特性,就是为了和抽象方法做区分,如果不写的话,编译器会自动将其识别为public abstract的抽象方法。
2.接口特性:
? ? ? ? 接口并不能直接使用,必须有一个类来实现它,实现的关系通过关键字implements来完成。
? ? ? ? ?1.接口类型是一种引用类型,不能直接使用new关键字来实例化接口。、
? ? ? ? ?2.接口当中的每一个没有实现的方法都是public的抽象方法,即接口当中的方法会被隐式地指定为public abstract,当然我刚才提到的默认方法是Java8之后的一个特性,大家在这里自行做一个区分。
? ? ? ? 3.接口当中的方法是不能在接口当中实现的,只能由类来实现。
? ? ? ? 4.当我们重写接口当中的方法的时候只能使用public访问权限,注意这里非常重要,即便是重写接口当中的默认方法,也只能使用public修饰,因为接口就是为了提供一种规范供所有人使用,即便是默认方法也是为了让大家去实现的,所以重写接口的方法必须是public修饰。
? ? ? ? 5.接口当中的变量会被隐式地指定为public static final变量,说白了也就是常量,接口当中定义的变量其实都是常量,编译器不允许修改,还是之前的原因,接口就是为了提供一种规范,如果接口当中的属性可以随意修改那就乱套了,大家都知道秦始皇同意度量衡,书同文、车同轨,就是为了提供一种规范,那么大家觉得这种规范我们作为程序员可以随意修改吗?显然是不行的。
? ? ? ? 6.接口当中不可以有静态代码块和构造方法。首先我们先说为什么不能定义构造方法,很简单,大家还记得构造函数的作用是什么吗?是不是就是为了实例化对象的时候为这个实例化对象进行初始化的?这就是构造函数的作用,那么接口是不可以被实例化的,他就是一个规范,那么这样不能够被实例化的接口提供构造函数有什么意义呢?
? ? ? ? 那么我们再谈为什么接口当中不能够定义静态代码块呢?首先我先说结论,不只是静态代码块,即使是普通代码块,接口当中也不可以定义,接口当中主要为了提供一套规范,用于定义抽象方法、常量默认方法和静态方法。这些都是属于静态绑定,在程序的编译期间就已经绑定和确认了,在接口当中定义静态代码块和普通代码块,它们根本就没有执行的时机,况且接口属于类级别,编译后会加载到方法区内,但是普通代码块是在实例化对象的时候执行的,它们属于实例的初始化阶段属于对象的初始化过程,而接口不能够被初始化,所以自然也就不能定义普通代码块,静态代码块自然也不行,虽然静态代码块不依赖于对象的实例,但是接口的存在就是为了提供一套规范,并不是为了执行某种逻辑存在的,所以在接口当中定义代码块不论是什么类型的代码块都是不被允许的。
? ? ? ? 7.接口虽然不是类,但是接口编译完成后的字节码文件的后缀格式也是.class。
? ? ? ? 8.如果类没有实现接口中的抽象方法,则类必须设置为抽象类。
? ? ? ? 9.接口当中还可以包含default方法(刚才已经讲过了)。
?3.多接口实现:
????????我们都知道类和类之间是单继承的,一个类只允许有一个父类,但是接口不一样,Java当中不支持多继承,但是一个类当中是可以有多个接口的,也就是说一个类可以实现多个接口。
interface IFly{ void fly(); } interface IRun{ void run(); } public class Animal implements IFly,IRun{ @Override public void fly() { System.out.println("我会飞"); } @Override public void run() { System.out.println("我会跑"); } }
? ? ? ? 多继承的实现代码已经给大家展示出来了,就这些内容。
?4.接口继承:
????????在Java当中类和类之间是单继承的,一个类可以实现多个接口,接口于接口之间可以多继承,接口之间的继承同样也是extends来实现的。我用一段代码来为大家展示一下,接口之间的继承,这段代码里面涉及到了默认方法,所以大家需要将我之前讲的有关默认方法的知识全部吸收,否则这里会理解困难。
package Demo2; interface Interface1{ void method1(); default void defaultMethod(){ System.out.println("Default method in Interface1"); } } interface Interface2{ void method2(); default void defaultMethod2(){ System.out.println("Default method in Interface2"); } } interface ExtendsInterface extends Interface1,Interface2{ void additionalMethod(); @Override default void defaultMethod() { //Interface1.super.defaultMethod(); System.out.println("我重写了第一个接口当中的默认方法!"); } @Override default void defaultMethod2() { //Interface2.super.defaultMethod2(); System.out.println("我重写了第二个接口当中的默认方法!"); } } class MyClass implements ExtendsInterface{ @Override public void method1() { System.out.println("这个类实现了接口当中的method1()方法!"); } @Override public void method2() { System.out.println("这个类实现了接口当中的method2()方法!"); } @Override public void additionalMethod() { System.out.println("这个类当中实现了接口当中的method2()方法!"); } } public class Main { public static void main(String[] args) { MyClass myClassObj = new MyClass(); myClassObj.method1(); myClassObj.method2(); myClassObj.additionalMethod(); myClassObj.defaultMethod(); myClassObj.defaultMethod2(); } }
?5.使用接口实例化对象:
????????使用接口的实例化类似于多态,通过向上转型,来访问子类重写父类的方法,一个类实现了这个接口,通过向上转型就可以访问类重写接口的方法,整体的用法类似于多态。这里就不进行过多地赘述了。
? ? ? ? 这里再为大家展示出一份相较于之前更为完整的一份代码。
package Demo2; interface Interface1{ void method1(); default void defaultMethod(){ System.out.println("Default method in Interface1"); } } interface Interface2{ void method2(); default void defaultMethod2(){ System.out.println("Default method in Interface2"); } } interface ExtendsInterface extends Interface1,Interface2{ void additionalMethod(); @Override default void defaultMethod() { //Interface1.super.defaultMethod(); System.out.println("我重写了第一个接口当中的默认方法!"); } @Override default void defaultMethod2() { //Interface2.super.defaultMethod2(); System.out.println("我重写了第二个接口当中的默认方法!"); } } class MyClass implements ExtendsInterface{ @Override public void method1() { System.out.println("这个类实现了接口当中的method1()方法!"); } @Override public void method2() { System.out.println("这个类实现了接口当中的method2()方法!"); } @Override public void additionalMethod() { System.out.println("这个类当中实现了接口当中的额外默认方法!"); } @Override public void defaultMethod(){ System.out.println("在继承接口重写的基础之上我又一次重写了第一个接口的默认方法"); } @Override public void defaultMethod2(){ System.out.println("在继承接口重写的基础之上我又一次重写了第二个接口的默认方法"); } } public class Main { public static void main(String[] args) { MyClass myClassObj = new MyClass(); myClassObj.method1(); myClassObj.method2(); myClassObj.additionalMethod(); myClassObj.defaultMethod(); myClassObj.defaultMethod2(); System.out.println("================================"); ExtendsInterface extendsInterfaceObj = new MyClass(); extendsInterfaceObj.additionalMethod(); extendsInterfaceObj.defaultMethod(); extendsInterfaceObj.defaultMethod2(); } }
?6.静态方法:
????????Java8以及更高的更新版本使用接口的时候,我们可以在接口当中定义静态方法,这些静态方法是于接口本身相关的,而不是于实现接口的类相关的。
interface IUsb{ static void Print(){ System.out.println("Hello World!"); } }
? ? ? ? 静态方法不需要再类当中实现,可以直接通过接口的名称来进行调用这些静态方法,不需要创建接口的实例。使用这种方式,接口可以提供一些于接口本身直接相关的使用方法,而不必依赖于实现类的实例,这对于定义一些通用的辅助或工具方法非常有用。
?public class Main { public static void main(String[] args) { IUsb.Print(); } } interface IUsb{ static void Print(){ System.out.println("Hello World!"); } }
? ? ? ? 代码如上图所示,这就是接口当中的抽象类静态方法的使用方式,就为大家介绍到这里。
?总结
? ? ? ? Java当中抽象类和接口就为大家介绍到这里,还有关于深浅拷贝和Object类的相关内容我放到下一篇文章再去讲解,离过年不远了,我也要准备开始我的《编译原理》专栏的准备工作了,争取在阴历年前把编译原理的所有重点全部系统地整理出来,真的很久没有用心地去准备做一件事情了,自律的快乐超过了所有物质享受给我带来的快乐,各位小伙伴们一起加油!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!