接口概述
接口是Java语言中的一种引用类型,是方法的"集合",所以接口的内部主要就是定义方法,包含常量,抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法(jdk9)。接口中不能有其他成员,没有构造器,没有初始化块,因为接口中没有成员变量需要初始化。接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。接口的使用,它不能创建对象,但是可以被实现(implements ,类似于被继承)。一个实现接口的类(可以看做是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象类。
接口的声明格式
接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要...则必须能...”的思想。继承是一个"是不是"的is-a关系,而接口实现则是 "能不能"的has-a关系。 ?
代码示例
public interface IA {
// 常量(jdk7及其以前) 使用public static final关键字修饰,这三个关键字都可以省略
public static final int A = 18;
int B = 20;
// 抽象方法(jdk7及其以前) 使用public abstract关键字修饰,这2个关键字都可以省略
public abstract void method1();
void method2();
// 默认方法(jdk8) 使用public default关键字修饰,public可以省略,default不可以省略
public default void method3() {
System.out.println("我是默认方法1");
}
default void method4() {
System.out.println("我是默认方法2");
}
// 静态方法(jdk8) 使用public static关键字修饰,public可以省略,static不可以省略
public static void method5() {
System.out.println("我是静态方法1");
}
static void method6() {
System.out.println("我是静态方法2");
}
// 私有方法(jdk9) 使用private关键字修饰,private不可以省略
private static void method7() {//私有静态方法
System.out.println("私有静态方法 method5");
}
private void method8() {//普通私有方法
System.out.println("私有非静态方法 method6");
}
}
实现接口
接口的使用,它不能创建对象,但是可以被实现(implements ,类似于被继承)。 类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements 关键字。如果某个功能是一个类额外增加的,那么就可以把这个额外的功能定义到接口中,再这个类去实现
实现接口语法格式
注意:
- 如果接口的实现类是非抽象类,那么必须重写接口中所有抽象方法。
- 默认方法可以选择保留,也可以重写。重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了
- 不能重写静态方法
?代码示例
定义接口
public interface IA {
//静态常量
long MAX_SPEED = 500*1024*1024;//500MB/s
//抽象方法
void read();
void write();
//默认方法
public default void start(){
System.out.println("开始");
}
public default void stop(){
System.out.println("结束");
}
//静态方法
public static void show(){
System.out.println("USB 3.0可以同步全速地进行读写操作");
}
}
定义实现类
public class IAimp implements IA {
//重写/实现接口的抽象方法,【必选】
@Override
public void read() {
System.out.println("读数据");
}
@Override
public void write() {
System.out.println("写数据");
}
//重写接口的默认方法,【可选】
//重写默认方法时,default单词去掉
@Override
public void stop() {
System.out.println("清理硬盘中的隐藏回收站中的东西,再结束");
}
}
接口中成员的访问特点
接口中的常量: 主要是供接口直接使用,我们可以通过接口名称丶实现类对象丶实现类访问,推荐使用接口名访问
interface A {
public static final double PI = 3.1415926;
}
class Aimp implements A {
//接口中的常量被实现类继承了
}
public class Test {
public static void main(String[] args) {
// 推荐,接口名称的方式访问
System.out.println(A.PI);
System.out.println(Aimp.PI);
System.out.println(new Aimp().PI);
}
}
为什么可以通过实现类和实现类对象去访问,因为接口中的常量是被static修饰,且被实现类继承下来了。
接口中的抽象方法:供实现类重写的,只能通过实现类对象才可以调用
interface A{
void eat();
}
class Aimp implements A{
@Override
public void eat() {//重写接口中的抽象方法
System.out.println("吃东西");
}
}
public class Test {
public static void main(String[] args) {
//创建实现类对象,调用重写后的抽象方法
new Aimp().eat();
}
}
接口中的默认方法:供实现类继承的,实现类中可以直接调用,实现类对象也可以直接调用。如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法
interface A{
default void eat(){
System.out.println("父类吃窝窝头");
}
default void sleep(){
System.out.println("睡觉");
}
}
class Aimp implements A{
@Override
public void eat() {//重写接口中的默认方法
sleep();//直接调用父类中的默认方法
System.out.println("子类吃白米饭");
}
}
public class Test {
public static void main(String[] args) {
//创建实现类对象,调用重写后的默认方法
new Aimp().eat();
}
}
接口中的静态方法:只能使用“接口名."进行调用,不能通过实现类的对象进行调用
interface A {
static void sleep() {//静态方法
System.out.println("睡觉");
}
}
class Aimp implements A {
}
public class Test {
public static void main(String[] args) {
//直接使用“接口名.”进行调用即可
A.sleep();
// new Aimp().sleep();不能通过实现类的对象进行调用
}
}
接口中的私有方法: 只能在接口中直接调用,实现类继承不了。因为有了默认方法和静态方法这样具有具体实现的方法,那么就可能出现多个方法由共同的代码可以抽取,而这些共同的代码抽取出来的方法又只希望在接口内部使用,所以就增加了私有方法。
接口的多实现
在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。
多实现时如果发生了多个父接口中有相同的常量或者方法产生了冲突怎么办?
公有静态常量的冲突:如果多个父接口有相同的常量,那么实现类就无法继承。简而言之:不能通过实现类或者实现类对象调用相同的常量,因为引用模糊不清。
interface A {
public static final int NUM1 = 10;
}
interface B {
public static final int NUM1 = 20;
public static final int NUM2 = 30;
}
class ABimp implements A, B {
}
public class Test {
public static void main(String[] args) {
System.out.println(A.NUM1); //10
System.out.println(B.NUM1);//20
System.out.println(B.NUM2);//30
//System.out.println(ABimp.NUM1);错误
System.out.println(ABimp.NUM2);//30
}
}
一般情况,父接口中的常量都会被子类继承,除非出现常量冲突问题。
公有抽象方法的冲突:接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果多个父接口中抽象方法有重名的,只需要重写一次。
interface A{
public abstract void method();
}
interface B{
public abstract void method();
}
class Imp implements A,B{
@Override
public void method() {
System.out.println("实现类重写");
}
}
public class Test {
public static void main(String[] args) {
/*
公有抽象方法的冲突:实现类只需要重写一个
*/
}
}
公有默认方法的冲突:当一个类同时实现了多个接口,而多个接口中包含方法签名相同的默认方法时,实现类必须重写一次最终版本
interface A{
public default void method(){
System.out.println("A 接口的默认方法method");
}
}
interface B{
public default void method(){
System.out.println("B 接口的默认方法method");
}
}
class Imp implements A,B{
@Override
public void method() {
System.out.println("实现类重写的默认方法");
}
}
public class Test {
public static void main(String[] args) {
/*
公有默认方法的冲突:实现类必须重写一次最终版本
*/
Imp imp = new Imp();
imp.method();
}
}
公有静态方法的冲突:静态方法是直接属于接口的,不能被继承,所以不存在冲突
私有方法的冲突:私有方法只能在本接口中直接使用,不存在冲突
接口和接口的关系
接口与接口之间的关系 :接口可以“继承”自另一个“接口”,而且可以“多继承”。
interface A {
}
interface B {
}
interface C1 extends A {
} // 单继承
interface C2 extends A, B {
} // 多继承
interface C3 extends C2 {
} // 多层继承
public class Test {
public static void main(String[] args) {
/*
- 接口与接口之间的关系: 继承关系
单继承: A接口继承B接口
多继承: A接口同时继承B接口,C接口,...
多层继承: A接口继承B接口,B接口,继承C接口
*/
}
}
接口多继承接口的时候,如果发生了多个父接口中有相同的常量或者方法产生了冲突怎么办?
- 公有静态常量的冲突:子接口无法继承父接口中冲突的常量
- 公有抽象方法的冲突:子接口只会继承一个有冲突的抽象方法
- 公有默认方法的冲突:子接口中必须重写一次有冲突的默认方法,实现类重写接口中的默认方法,不需要加default,?子接口重写父接口中的默认方法,必须加default。
- 公有静态方法和私有方法:不冲突,因为静态方法是直接属于接口的,只能使用本接口直接访问,而私有方法只能在接口中访问,也没有冲突
实现类继承父类又实现接口时,父接口中和父类中有相同的常量或者方法产生了冲突怎么办?
- 父类和接口的公有静态常量的冲突:子类无法继承有冲突的常量
class Fu{
public static final int NUM1 = 10;
public static final int NUM2 = 100;
}
interface A{
public static final int NUM1 = 20;
}
class Zi extends Fu implements A{
}
public class Test {
public static void main(String[] args) {
/*
公有静态常量的冲突:子类无法继承有冲突的常量
*/
//System.out.println(Zi.NUM1);// 编译报错
System.out.println(Zi.NUM2);
}
}
- 父类和接口的抽象方法冲突:子类必须重写一次有冲突的抽象方法
abstract class Fu{
public abstract void method();
}
interface A{
public abstract void method();
}
class Zi extends Fu implements A{
@Override
public void method() {
System.out.println("Zi 重写有冲突的抽象方法");
}
}
public class Test {
public static void main(String[] args) {
/*
公有抽象方法的冲突:子类必须重写一次有冲突的抽象方法
*/
Zi zi = new Zi();
zi.method();
}
}
- 父类和接口的公有默认方法的冲突:父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。
- 父类和接口的公有静态方法:子类只能访问父类的静态方法
- 父类和接口的私有方法:子类都无法访问,不存在冲突
应用场景:
- 额外的功能:在接口中定义,让实现类实现,如果某个功能是一个类额外增加的,那么就可以把这个额外的功能定义到接口中,再这个类去实现
- 共性的功能:在父类中定义,让子类继承,如果一个父类中的某个方法,所有子类都有不同的实现,那么该方法就应该定义成抽象方法,所以该父类就是抽象类 (父类一般都是抽象类)
多态
多态是继封装、继承之后,面向对象的第三大特性。生活中,比如求面积的功能,圆、矩形、三角形实现起来是不一样的。跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来也是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。那么此时就会出现各种子类的类型。
定义
- 多态: 是指同一行为,对于不同的对象具有多个不同表现形式。
- 程序中多态: 是指同一方法,对于不同的对象具有不同的实现.
多态的由来:
Java是强类型静态语言,既每一个变量在使用之前必须声明它确切的类型,然后之后的赋值和运算时都是严格按照这个数据类型来处理的。例如:
int num = 10;
但是,有的时候,我们在设计一个数组、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。例如:想要设计一个数组用来存储各种图形的对象,并且按照各种图形的面积进行排序,但是具体存储的对象可能有圆、矩形、三角形等,那么各种图形的求面积方式又是不同的。例如:想要设计一个方法,它的功能是比较两个图形的面积大小,返回面积较大的那个图形对象。那么此时形参和返回值类型是图形类型,但是不知道它具体是哪一种图形类型。这个时候,Java就引入了多态。
同时满足下面的条件就可以实现多态在程序中的应用
- 继承或者实现【二选一】
- 父类引用指向子类对象或者接口引用指向实现类对象【格式体现】
- 方法的重写【意义体现:不重写,无意义】
代码示例
abstract class Animal {//父类
public abstract void eat();
}
//子类
class Dog extends Animal{
@Override
public void eat() {//方法的重写
System.out.println("狗吃骨头");
}
}
//子类
class Cat extends Animal{
@Override
public void eat() {//方法的重写
System.out.println("猫吃鱼");
}
}
public class Test {
public static void main(String[] args) {
//父类引用指向子类对象
Animal dog = new Dog();
dog.eat(); //狗吃骨头
//父类引用指向子类对象
Animal cat = new Cat();
cat.eat();//猫吃鱼
}
}
多态时访问成员的特点
如果直接访问成员变量,那么只看编译时类型,即编译看左边,运行看左边 。简而言之:多态的情况下,访问的是父类的成员变量,如果父类中没有要访问的成员变量则报错
class Animal {
int age = 18;
}
class Dog extends Animal {
int age = 20;
String name = "金毛";
}
public class Test {
public static void main(String[] args) {
Animal dog = new Dog();
System.out.println(dog.age);//18
//System.out.println(dog.name); 编译错误
}
}
多态时成员方法的访问特点
在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。当我们通过“对象.方法”的形式,调用一个虚方法,我们要如何确定它具体执行哪个方法呢?
- 静态分派:先看这个对象的编译时类型,在这个对象的编译时类型中找到最匹配的方法最匹配的是指,实参的编译时类型与形参的类型最匹配。如果找不到则编译失败
- 动态绑定:再看这个对象的运行时类型,如果这个对象的运行时类重写了刚刚找到的那个最匹配的方法,那么执行重写的,否则仍然执行刚才编译时类型中的那个方法
简而言之:编译的时候去父类中查找方法,运行的时候去子类中查找方法来执行
class Animal {
public void eat(){
System.out.println("吃东西");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
public class Test {
public static void main(String[] args) {
Animal dog = new Dog();
dog.eat();//狗吃骨头
}
}
下面几种方法,编译的时候去父类中查找方法,运行的时候去父类中查找方法来执行。
- static方法,这种方法在编译时确定在运行时不会改变。
- 由invokespecial指令调用的方法,这些方法包括私有方法,实例构造方法和父类方法,这些方法也是在编译时已经确定,在运行时不会再改变的方法
- 由final关键字修饰的方法。虽然final方法是由invokevirtual指令进行调用的,但是final修饰的方法不能够进行在子类中进行覆盖,所以final修饰的方法是不能够在运行期进行动态改变的。
多态的几种表现形式
普通父类多态:
class Fu{}
class Zi extends Fu{}
public class Demo{
public static void main(String[] args){
Fu f = new Zi();//左边是一个“普通父类”
}
}
抽象父类多态
abstract class Fu{}
class Zi extends Fu{}
public class Demo{
public static void main(String[] args){
Fu f = new Zi();//左边的父类是抽象类
}
}
父接口多态
interface A{}
class AImp implements A{}
public class Demo{
public static void main(String[] args){
A a = new AImp();//左边是一个父接口
}
}
多态的几种应用场景:
场景一:变量多态,如果变量的类型为父类类型,该变量就可以接收该父类类型的对象或者其所有子类对象
class Animal{
public void eat(){
System.out.println("吃东西...");
}
}
class Cat extends Animal{
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
public class Test {
public static void main(String[] args) {
// 变量多态: 父类类型的变量指向子类类型的对象
// 如果变量的类型为父类类型,该变量就可以接收该父类类型的对象或者其所有子类对象
anl = new Cat();
anl.eat();
}
}
场景二:形参多态,父类类型作为方法形式参数,子类对象为实参。
class Animal {
public void eat() {
System.out.println("吃东西");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
public class Test {
public static void main(String[] args) {
Animal dog = new Dog();
//实参赋值给形参的时候==> Animal anl = new Dog();
method(dog);
}
//定义一个方法,带有一个参数,该参数可以接收Animal类对象以及Animal类的所有子类对象
public static void method(Animal animal) {
animal.eat();//狗吃骨头
}
}
场景三:返回值多态,如果返回值类型为父类类型,那么就可以返回该父类类型的对象或者其所有子类对象
//父类
abstract class Animal {
abstract void eat();
}
//子类Dog
class Dog extends Animal {
@Override
void eat() {
System.out.println("狗吃骨头");
}
}
//子类Cat
class Cat extends Animal{
@Override
void eat() {
System.out.println("猫吃鱼");
}
}
public class Test {
public static void main(String[] args) {
buy("猫咪").eat();//猫吃鱼
}
/*
* 设计一个方法,可以购买各种动物的对象,此时不确定是那种具体的动物
*
* 返回值类型是父类的对象
*
* 多态体现在 返回值类型 Animal ,实际返回的对象是子类的new Cat(),或new Dog()
*/
public static Animal buy(String name){
if("猫咪".equals(name)){
return new Cat();
}else if("小狗".equals(name)){
return new Dog();
}
return null;
}
}
场景四:多态应用在数组,数组元素类型声明为父类类型,实际可以存储的其父类和子类对象
/*
* 声明一个数组,可以装各种动物的对象,
*/
Animal[] arr = new Animal[2]; //此时不是new Animal的对象,而是new Animal[]的数组对象
//在堆中开辟了长度为2的数组空间,用来装Animal或它子类对象的地址
arr[0] = new Cat();//多态引用 左边arr[0] 是Animal类型,右边是new Cat()
//把Cat对象,赋值给Animal类型的变量
arr[1] = new Dog();
多态的好处和弊端
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。 弊端:多态的情况下,只能调用父类的共性内容,不能调用子类的特有内容。所以,想要调用子类特有的方法,必须做类型转换。不管是向上转型还是向下转型,一定满足父子类关系或者实现关系
向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
向下转型:当左边的变量的类型(子类)<右边对象/变量的类型(父类),我们就称为向下转型
- 格式:子类类型 对象名 = (子类类型)父类类型的变量;
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型。此时,不一定是安全的,需要使用(类型)进行强制类型转换,这个过程是手动的。
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
代码示例
//父类
abstract class Animal {
abstract void eat();
}
//子类Dog
class Dog extends Animal {
@Override
void eat() {
System.out.println("狗吃骨头");
}
public void lookHome(){
System.out.println("狗看家");
}
}
//子类Cat
class Cat extends Animal{
@Override
void eat() {
System.out.println("猫吃鱼");
}
public void grabMouse(){
System.out.println("猫抓老鼠");
}
}
public class Test {
public static void main(String[] args) {
method(new Cat());//传入的是Dog对象
}
public static void method(Animal animal){
Dog dog = (Dog) animal;//这段代码可以通过编译,但是运行时,却报出了ClassCastException
//这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,
}
}
运行之后的结果如下图所示:
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
作用:
- 判断前面变量指向的对象类型是否是后面的数据类型:
- 如果前面变量指向的对象类型是属于后面的数据类型,那么就返回true
- 如果前面变量指向的对象类型不是属于后面的数据类型,那么就返回false
?所以,转换前,我们最好先做一个判断,代码如下:
public static void method(Animal animal) {
if (animal instanceof Cat) {
// 向下转型
Cat cat = (Cat) animal;
cat.eat();//猫吃鱼
// 调用的是 Cat 的 独有的grabMouse方法
cat.grabMouse();//猫抓老鼠
} else if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.eat();//狗吃骨头
// 调用的是 Dog 的 独有的lookHome方法
dog.lookHome();//狗看家
}
}
总结一下多态的弊端:无法访问子类独有的方法或者成员变量,因为多态成员访问的特点是,编译看父类。解决方式类型转换。
|