Java基础09—接口继承与多态

Posted xuliang-daydayup

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java基础09—接口继承与多态相关的知识,希望对你有一定的参考价值。

接口、继承与多态


参考资料:《Java从入门到精通》/明日科技编著. 4版. 北京:清华大学出版社,2016

一、类的继承

继承可以使得整个程序的架构具有一定的弹性,在程序中复用一些已经定义完善的类不仅可以减少软件开发周期,也可以提高软件的可维护性和可扩展性。

继承的基本思想:

  • 基于某个父类的扩展,制定出一个新的子类;
  • 子类拥有父类的属性和方法,也可以增加父类所不具备的属性和方法;
  • 子类可以重写父类的某些方法。
/*
*定义一个父类Test
*/
public class Test {
    //构造方法
    public Test(){
        System.out.println("I am father.");
    }
    //成员方法
    protected void doSomething(){
        System.out.println("I cam teach my son.");
    }
    //成员方法,返回一个Test对象
    protected Test dolt(){
        return new Test();
    }
}

/*
 *定义一个子类Test2,继承Test
 */
class Test2 extends Test{
    //构造方法
    public Test2(){
        //调用父类的构造方法
        super();
        //调用父类的成员变量
        super.doSomething();
    }
    //定义子类的成员方法
    public void doSomethingNew(){
        System.out.println("I am son.");
    }
    //重写父类的成员方法
    public void doSomething(){
        System.out.println("This is son, i can cover my father.");
    }
    //重写父类的成员方法,返回一个Test2对象
    protected Test2 dolt(){
        return new Test2();
    }
}
  • 在子类中,可以使用“super()”语句调用父类的构造方法;
  • 在子类中,可以使用“super.方法名”调用父类的成员方法;
  • 子类没有权限调用父类中被修饰为private的方法;
  • 子类只可以调用父类中修饰为“public”和“protected”的成员方法;
  • 子类可以重写父类的功能。重写就是在子类中保留父类的成员方法的名称,重写成员方法的实现内容。
  • 重构:一种特殊的重写方式,只重写成员方法的实现方式,其他如成员方法返回值、参数类型等与父类完全一致。

注意:当重写父类的成员方法时,修改成员方法的修饰权限只能从小的范围到大的范围改变。例如,父类成员方法的修饰符为protected,那子类中的方法的修饰符只能为public或protected,不能为private。

一句话概括,重写时不能缩小修饰符的范围。

在Java中一切以对象的形式进行处理,在继承的机制中,创建一个子类对象,将包含一个父类子对象,这个对象与父类创建的对象一致。两者的区别在于后者来源于外部,而前者来自于子类对象的内部。当实例化子类对象时,父类对象也相应被实例化。换句话说,在实例化子类对象时,Java编译器会在子类的构造方法中自动调用父类的无参构造方法

class grandFather {
    grandFather(){
        System.out.println("调用grandFather类的构造方法");
    }
}

class Father extends grandFather {
    Father(){
        System.out.println("调用Father类的构造方法");
    }
}

public class Son extends Father {
    Son(){
        System.out.println("调用Son类的构造方法");
    }

    public static void main(String[] args) {
        Son son = new Son();
    }
}

输出结果:
调用grandFather类的构造方法
调用Father类的构造方法
调用Son类的构造方法

从上述代码的运行结果来看,在实例化子类的对象的同时调用父类的构造方法。调用父类构造方法的顺序为:首先是顶级父类,然后是上一层父类,最后是子类。也就是说,实例化子类对象时,首先要实例化父类对象,然后再实例化子类对象。所以在子类构造方法去访问父类的构造方法之前,父类已经完成实例化操作。

说明:在实例化子类对象时,父类的无参构造方法被自动调用,但是有参构造方法并不能被自动调用,只能依赖于super关键字显示地调用父类的构造方法。

class grandFather {
    grandFather(){
        System.out.println("调用grandFather类的构造方法");
    }
}

class Father extends grandFather {
    Father(){
        System.out.println("调用Father类的构造方法");
    }
    //有参构造方法
    Father(int a){
        System.out.println("调用Father类的有参构造方法");
    }
}

public class Son extends Father {
    Son(){
        super(10);   //调用父类的有参构造方法
        System.out.println("调用Son类的构造方法");
    }
    //有参构造方法
    Son(int a){
        super(10);   //调用父类的有参构造方法
        System.out.println("调用Son类的有参构造方法");
    }

    public static void main(String[] args) {
        Son son = new Son(10);   //调用子类有参构造方法
    }
}

输出结果:
调用grandFather类的构造方法
调用Father类的有参构造方法
调用Son类的有参构造方法

注意:super()语句只能位于构造方法中,且必须是第一条语句

二、Object类

  • 在Java中,所有的类都是直接或间接继承了java.lang.Object类。
  • Object类是比较特殊的类,它是所有类的父类,是Java类层中的最高层类。
  • 当创建一个类时,总是在继承,除非是某个类已经指定要从其他类继承,否则它就是从java.lang.Object类继承而来的。比如String类、Integer类等都是继承于Object类。
  • 自定义的类也是继承于Object类,只是省略了extends Object语句。
class Student {
    //......
}

等价于:

class Student extends Object {
    //......
}

在Object类中主要包括clone()、finalize()、equals()、toString()等方法。其中最常用的方法为equals()、toString()方法。由于所有类都是Object类的子类,所以任何类都可以重写Object类中的方法。

注意:Object类中被定义为final类型的方法不能重写,如getClass()、notify()、notifyAll()、wait()方法等。

下面重点介绍Object类中比较重要的几个方法:

1、getClass()方法

该方法返回对象执行时的Class实例,然后使用此实例调用getName()方法可以取得类的名称。语法如下:

getClass().getName();
Son son = new Son();
String str = son.getClass().getName();
System.out.println(str);   //打印类的名称

输出结果:
com.xuwenjia.Son

2、toString()方法

该方法将一个对象返回为字符串形式,它会返回一个String实例。

public class ObjectInstance {
    //重写toString()方法
    public String toString(){
        return "在" + getClass().getName() + "类中重写toString方法";
    }

    public static void main(String[] args) {
        ObjectInstance objectInstance = new ObjectInstance();
        //自动调用toString(),相当于objectInstance.toString()
        System.out.println(objectInstance);   
    }
}

输出结果:
在com.xuwenjia.ObjectInstance类中重写toString方法

3、equals()方法

之前提到过,“==”运算符是比较两个对象的引用是否相等,而equals()方法比较两个对象的实际内容。

public class Equals {
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        System.out.println(str1.equals(str2));   //打印true

        String str3 = new String("abc");
        String str4 = new String("abc");
        System.out.println(str3.equals(str4));   //打印true

        String str5 = new String();
        String str6 = new String();
        System.out.println(str5.equals(str6));   //打印true

        Equals e1 = new Equals();
        Equals e2 = new Equals();
        System.out.println(e1.equals(e2));    //打印false
    }
}

从上述代码的执行结果看,在自定义的类中使用equals()方法进行比较时,将返回false,这是因为equals()方法默认实现是使用“==”运算比较符比较两个对象的引用地址,而不是比较对象的实际内容。所以想要真正做到比较两个对象的内容,需要在自定义类中重写equals()方法。

三、对象类型的转换

对象类型的转换主要包括向上转型与向下转型操作。

1、向上转型

因为平行四边形是特殊的四边形,可以将平行四边形对象看作是一个四边形对象。

//定义Quadrangle父类
class Quadrangle {
    //定义静态成员方法draw()
    public static void draw(Quadrangle quadrangle){
        System.out.println("i can draw Quadrangle.");
    }
}
//定义Parallelogram类,并继承Quadrangle类
public class Parallelogram extends Quadrangle{
    public static void main(String[] args) {
        //创建Parallelogram对象
        Parallelogram parallelogram = new Parallelogram();
        //调用父类成员方法,并传入参数
        draw(parallelogram);
    }
}

输出结果:
i can draw Quadrangle.

在上述代码中,父类存在一个draw()方法,它的参数是Quadrangle类型对象,而子类在调用draw()方法时传入的参数却是Parallelogram类型对象。

这里强调一个问题,子类的对象可以看作是一个父类的对象,相当于“Quadrangle qua = new Parallelogram();”。同样的,正方形类对象可以作为draw()方法的参数,梯形类对象也可以作为draw()方法的参数。

这种把子类对象赋值给父类类型的变量称为向上转型。由于向上转型是一个从较为具体的类到较抽象的类的转换,所以它总是安全的。

2、向下转型

向下转型是较为抽象的类转化为叫具体的类,这种转型通常会出现问题。可以说子类对象是父类的一个实例,但是父类不一定是子类的实例。

//定义Quadrangle父类
class Quadrangle {
    //定义静态成员方法draw()
    public static void draw(Quadrangle quadrangle){
        System.out.println("i can draw Quadrangle.");
    }
}
//定义Parallelogram类,并继承Quadrangle类
public class Parallelogram extends Quadrangle{
    public static void main(String[] args) {
        //创建Parallelogram对象
        Parallelogram parallelogram = new Parallelogram();
        //调用父类成员方法,并传入参数
        draw(parallelogram);
        /*
         * 将Parallelogram对象看作Quadrangle对象,也称为向上转型
         * 虽然声明的类型为Quadrangle类,但还是调用的Parallelogram类的构造方法
         * 所以,实际上还是创建了一个Parallelogram对象
         * 如果下面的代码改成调用父类的构造方法,即new Quadrangle(),则最后一条语句编译不通过
        */
        Quadrangle quadrangle = new Parallelogram();
        //将父类对象赋给子类对象,下面的写法是错误的
        //Parallelogram p = quadrangle;
        //将父类对象赋给子类对象,并强制转换为子类型,下面的写法正确
        Parallelogram p = (Parallelogram)quadrangle;
        //调用父类成员方法,并传入参数
        draw(p);
    }
}

输出结果:
i can draw Quadrangle.
i can draw Quadrangle.

从上述代码可以看到,如果将父类的对象直接赋予子类,会发生编译错误,因为父类对象不一定是子类的实例。

一般来说,越是具体的对象具有的属性越多,越抽象的对象具有的特性越少。在做向下转型的操作时,将特性范围越小的对象转换为特性范围越大的对象肯定会出现问题。所以当程序使用向下转型操作时,必须使用显示类型转换,向编译器指明父类对象转换为哪一种类型的子类对象。

注意:在上述代码中,向下转换操作的成功,除了使用显示转换,还有一个前提条件,即必须使用子类的构造方法。换一种说法就是,父类对象必须是子类对象的实例。

Quadrangle quadrangle = new Parallelogram();   //quadrangle虽然被声明是一个Quadrangle类对象,但实际上却是Parallelogram类对象
Parallelogram p = (Parallelogram)quadrangle;    //该语句执行成功

Quadrangle quadrangle = new Quadrangle();
Parallelogram p = (Parallelogram)quadrangle;    //该语句执行失败

四、使用instanceof操作符判断对象类型

当程序在执行向下转型操作时,如果父类对象不是子类对象的实例,就会发生ClassCastException异常,所以在执行向下转型之前需要养成一个良好的习惯,就是判断父类对象是否为子类对象的实例,这个判断通常使用instanceof操作符来完成。

  • 可以使用instanceof操作符判断是否一个类实现了某个接口;
  • 可以使用instanceof操作符判断一个实例对象是否属于一个类。

instanceof的语法格式如下所示:

myobject  instanceof  ExampleClass
  • myobject:某个类的对象引用
  • ExampleClass: 某个类的名称

使用instanceof操作符的表达式返回值为boolean值。如果返回值为true,说明myobject对象是ExampleClass类的实例对象;如果返回值为false,则不是。

注意:instanceof是Java语言的关键字,在Java语言中的关键字都为小写。

//定义Quadrangle父类
class Quadrangle {
    //定义静态成员方法draw()
    public static void draw(Quadrangle quadrangle){
        System.out.println("i can draw Quadrangle.");
    }
}
//定义Square类,并继承Quadrangle类
class Square extends Quadrangle {
    public static void draw(){
        System.out.println("i can draw Square.");
    }
}
//定义Parallelogram类,并继承Quadrangle类
public class Parallelogram extends Quadrangle{
    public static void main(String[] args) {
    
        Quadrangle quadrangle = new Quadrangle();
        
        if (quadrangle instanceof Parallelogram){
            System.out.println("为子类Parallelogram的对象实例");
            //执行向下转型操作
            Parallelogram p = (Parallelogram)quadrangle;
        }
        else if (quadrangle instanceof Square){
            System.out.println("为子类Square的对象实例");
            //执行向下转型操作
            Square s = (Square)quadrangle;
        }
        else {
            System.out.println("都不是");
        }
    }
}

输出结果:
都不是

五、方法的重载

之前学过构造方法,知道构造方法的名称已经由类名决定,所以构造方法只有一个名称,但是如果希望以不同的方式来实例化对象,就需要使用多个构造方法来实现。为了让方法名相同而形参不同的构造方法同时存在,必须用到“方法重载”。

方法的重载就是为了在同一个类中允许同时存在多个同名的方法,前提是这些方法的参数个数或类型都不同。

public class OverLoadTest {
    //第一个add()
    public static int add(int a, int b){     
        return a + b;
    }
    //第二个add()
    public static double add(double a, double b){     
        return a + b;
    }
    //第三个add()
    public static double add(int a, double b){
        return a + b;
    }

    public static void main(String[] args) {
        System.out.println(add(3,4));       //调用第一个add()
        System.out.println(add(5.0,6.0));   //调用第二个add()
        System.out.println(add(7,8.0));     //调用第三个add()
    }
}

输出结果:
7
11.0
15.0

可以构成重载的条件:

  • 参数类型不同
  • 参数的个数不同
  • 参数的顺序不同

综上所述,编译器总是利用方法名、参数类型、参数个数、参数顺序来确定类中的方法是否唯一。方法的重载使得方法以统一的名称被管理,使得代码有条理。

另外,可以定义不定长参数的方法,即参数的个数不固定,其语法格式如下:

返回值  方法名 (参数数据类型...参数名称)
    public static int add(int...a){   //定义不定长参数方法
        int sum = 0;
        for (int i = 0; i < a.length; i++) {
            s+=a[i];
        }
        return sum;
    }

在参数列表中使用“...”形式定义不定长参数,其实这个不定长参数a就是一个数组,编译器会将(int...a)这种形式看作是(int[]a),所以在add()方法体中做累加操作时使用了for()循环。

public class OverLoadTest {
    public static int add(int a, int b){
        return a + b;
    }
    public static double add(double a, double b){
        return a + b;
    }
    public static double add(int a, double b){
        return a + b;
    }
    //定义不定长参数方法
    public static int add(int...a){
        int sum = 0;
        for (int i = 0; i < a.length; i++) {
            sum+=a[i];
        }
        return sum;
    }

    public static void main(String[] args) {

        //调用不定长参数方法
        System.out.println(add(1,2,3,4,5,6,7,8,9));
        System.out.println(add(1,2,3));
        System.out.println(add(1));
    }
}

输出结果:
45
6
1

六、多态

利用多态可以使得程序具有良好的扩展性,并可以对所有对象进行通用的处理。

为了更好的理解多态的设计思想,这里依然以四边形类举例。假如现在需要绘制一个平行四边形,就需要在平行四边形类中定义一个draw()方法。假如需要绘制一个正方形,则需要定义一个正方形类,并在类中添加draw()方法。这样会出现代码冗余的缺点,通过定义平行四边形类和正方形类来分别处理平行四边形和正方形的对象,也没有太大意义。

如果定义一个四边形类,让它处理所有继承该类的对象,根据“向上转型”的原则可以使每个继承四边形类的对象作为draw()方法的参数,然后在draw()方法中作出一些限定就可以根据不同的图形类对象绘制相应的图形,从而以更为通用的四边形类取代了平行四边形类和正方形类。

public class QuadrangleTest {
    //实例化数组对象,用来保存四边形类型对象
    private QuadrangleTest[] quadrangleTests = new QuadrangleTest[6];
    private  int nextIndex = 0;
    public void draw(QuadrangleTest q){
        if (nextIndex < quadrangleTests.length){
            quadrangleTests[nextIndex] = q;
            System.out.println(nextIndex);
            nextIndex++;
        }
    }

    public static void main(String[] args) {
        QuadrangleTest quadrangleTest = new QuadrangleTest();
        //以正方形类对象为参数调用draw()方法
        quadrangleTest.draw(new SquareTest());
        //以平行四边形类对象为参数调用draw()方法
        quadrangleTest.draw(new ParallelogramTest());
    }

}
//定义正方形类,并继承四边形类
class SquareTest extends QuadrangleTest {
    public SquareTest(){
        System.out.println("正方形");
    }
}
//定义平行四边形类,并继承四边形类
class ParallelogramTest extends QuadrangleTest {
    public ParallelogramTest(){
        System.out.println("平行四边形");
    }
}

输出结果:
正方形
0
平行四边形
1

注意事项

  1. 多态是方法的多态,属性没有多态;
  2. 必须存在继承关系;
  3. 父类引用指向子类对象。

七、抽象类和接口

可以说四边形为具有4条边的图形,再具体一点,平行四边形是具有对边平行且相等特性的特殊四边形。但是对于图形类却不能使用具体的语言进行描述,究竟是什么图形?具有几条边?没人能说清楚,类似的还有动物类等,这种类在Java中被定义为抽象类。

1、抽象类

  • 在解决实际问题时,一般将父类定义为抽象类,需要使用这个父类进行继承和多态处理。
  • 在继承树中,越是往上的类越抽象,如鸽子类继承鸟类,鸟类继承动物类等。
  • 在多态机制中,并不需要将父类进行实例化,我们需要的是子类对象。因此,在Java语言中设置抽象类不可以实例化,因为图形类不能实例化任何一个具体的图形,动物类不能实例化一种具体的动物,但是它们的子类可以。

抽象类的语法格式如下所示:

public abstract class Test {
    abstract void testAbStract();     //定义抽象方法
}

其中,abstract是定义抽象类的关键字。

  • 使用abstract关键字定义的类称为抽象类;
  • 使用abstract关键字定义的方法称为抽象方法;
  • 抽象方法本身没有方法体,除非被重写;
  • 如果声明一个方法为抽象方法,那承载该方法的类必须定义为抽象类,换句话说,只要类中有一个抽象方法,此类就被标记为抽象类。
  • 抽象类被继承后,需要实现其中所有的抽象方法,也就是保证相同的方法名、相同的参数列表和相同的返回值并创建出非抽象的方法,即在方法体中添加具体的实现过程。
  • 继承抽象类的所有子类都需要将抽象类中的抽象方法进行覆盖。

注意:抽象类中可以有普通方法,普通方法不需要被实现。

public abstract class Test {
    abstract void testAbStract();     //定义抽象方法
    
    public void add(){               //定义普通方法
         System.out.println("普通方法");
    }
}

2、接口

接口是抽象类的延伸,可以将它看作是纯粹的抽象类,接口中的方法都没有方法体。

接口使用interface关键字进行定义,其语法形式如下:

public interface drawTest {
    void draw();    //接口内的方法,没有方法体,且省略abstract关键字
}
  • public:接口可以像类一样被权限修饰符修饰,但public关键字仅限用于在与其同名的文件中被定义。
  • interface:定义接口关键字
  • drawTest:接口名称

一个类实现一个接口可以使用implements关键字,如下所示:

public class Parallelogram extends Quadrangle implements drawTest {
    //.....
}

注意:在接口中定义的方法必须被定义为public或abstract形式,其他修饰权限不被Java编译器认可,即使不将该方法申明为public形式,它默认也是public。

说明:在接口中定义的任何字段都自动是static和final的。

//定义接口
interface drawTest{
    //定义方法
    public void draw();
}
//定义Quadrangle2类,该类继承了QuadrangleUseInterface类,并实现了drawTest接口
class Quadrangle2 extends QuadrangleUseInterface implements drawTest{
    public void draw(){
        System.out.println("平行四边形");
    }
}
//定义QuadrangleUseInterface类
public class QuadrangleUseInterface {
    public static void main(String[] args) {
        //父类引用指向子类对象,向上转型
        QuadrangleUseInterface q = new Quadrangle2();
        //父类没有draw()方法,则向下转型,调用子类draw()方法
        ((Quadrangle2)q).draw();
    }
}

输出结果:
平行四边形

接口也可以实现向上转型操作,如下所示:

//定义接口
interface drawTest{
    //定义方法
    public void draw();
}
//定义Quadrangle2类,该类继承了QuadrangleUseInterface类,并实现了drawTest接口
class Quadrangle2 extends QuadrangleUseInterface implements drawTest{
    public void draw(){
        System.out.println("平行四边形");
    }
}
//定义QuadrangleUseInterface类
public class QuadrangleUseInterface {
    public static void main(String[] args) {
        //接口实现向上转型操作
        drawTest d = new Quadrangle2();
        //调用draw()方法
        d.draw();
    }
}

输出结果:
平行四边形

在上述代码中,首先将Quadrangle2类型对象向上转型为drawTest接口形式,然后调用draw()方法。其实在Java中,无论是将一个类向上转型为父类对象,还是向上转型为抽象父类对象,或者向上转型为该类实现接口,都是没有问题的。

接口与继承

在Java中不允许多重继承,但使用接口就可以实现多继承,因为一个类可以同时实现多个接口,这样可以将所有需要继承的接口放在implements关键字后面并使用逗号隔开,但这可能在一个类中产生庞大的代码量,因为继承一个接口时需要实现接口中的所有方法。

多重继承的语法如下所示:

class 类名 implements 接口1, 接口2, 接口3...

在定义一个接口时,使该接口继承另外一个接口。

//定义接口1
interface drawTest{
    public void draw();
}
//定义接口2
interface drawTest2{
    public void draw2();
}
//定义接口3,并继承drawTest,drawTest2接口
interface drawTest3 extends drawTest, drawTest2{
    public void draw3();
}

//定义一个interfaceTest类,并实现drawTest3接口
class interfaceTest implements drawTest3{
    //实现接口1的方法
    public void draw(){
        System.out.println("实现接口1");
    }
    //实现接口2的方法
    public void draw2(){
        System.out.println("实现接口2");
    }
    //实现接口3的方法
    public void draw3(){
        System.out.println("实现接口3");
    }
}

以上是关于Java基础09—接口继承与多态的主要内容,如果未能解决你的问题,请参考以下文章

java基础讲解09-----接口,继承,多态

Java语言基础28-29--接口与多态

java中封装,继承,多态,接口学习总结

Java基础-- 继承 多态 泛型 接口 动态绑定 动态代理

java 代码片段

阶段1 语言基础+高级_1-3-Java语言高级_02-继承与多态_第2节 抽象类_9_接口的私有方法定义