Java面向对象编程·下

简介: Java面向对象编程·下

Java面向对象编程·下

大家好,我是晓星航。今天为大家带来的是面向对象编程相关的讲解!😀

这里我们从上篇的继承来为大家继续讲解😮:

⭐小提问解答⭐

首先我们要为大家讲解的便是我们上个文章为大家留下的小问题的解答了,不知道大家在上次看完这三个问题之后有没有自己的见解呢?!

1.查import和package的区别(通过import导包)

答:import:“引入”,指:引入类中需要的类。

package:“包” ,指:类所在的包

2.查两个相同的类名 怎么导入不报错?

答:省略import语句并使用整个路径引用它们。例如:

java.util.Date javaDate = new java.util.Date()

my.own.Date myDate = new my.own.Date();

但使用两个具有相同名称和类似功能的类通常不是最好的主意,除非你能清楚地知道它是哪个。

3.问一下两个是不是文件夹?

答:他们两个都是文件夹,并且可以在我们自己创建的仓库中以记事本的形式查看他们的代码。

4.重写和重载的区别?

答:在方法名,参数列表,返回类型都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。 而重载是同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。

5.this 和 super 以及this. 和 super.的区别?

答:this和super的区别:

1.属性的区别:

(1)this访问本类中的属性,如果本类没有这个属性则访问父类中的属性。

(2)super访问父类中的属性。

2.方法的区别:

(1)this访问本类中的方法,如果本类没有这个方法则访问父类中的方法。

(2)super访问父类中的方法。

3.构造的区别:

(1)this调用本类构造构造,必须放在构造方法的首行。

(2)super调用父类构造,必须放在子类构造方法首行。

(3)其他区别:this表示当前对象。super不能表示当前对象

this. 变量和super.变量

(1)this.变量:调用的当前对象的变量;

(2)super.变量:直接调用的是父类中的变量。

this(参数)和super(参数)方法

(1)this(参数):调用(转发)的是当前类中的构造器;

(2)super(参数):用于确认要使用父类中的哪一个构造器。

3.多态

多态:字面上理解:语文层级:一种事物多种形态。

3.1向上转型 - 父类引用子类对象

extends后面的类则表示父类,extends前面的类则是子类。在java中所有的类都默认继承Object类,Object类是所有类的父类。

在刚才的例子中, 我们写了形如下面的代码

Bird bird = new Bird("圆圆"); 

这个代码也可以写成这个样子

Bird bird = new Bird("圆圆");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");

此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型.

向上转型这样的写法可以结合 is - a 语义来理解.

例如, 我让我媳妇去喂圆圆, 我就可以说, “媳妇你喂小鸟了没?”, 或者 “媳妇你喂鹦鹉了没?”

因为圆圆确实是一只鹦鹉, 也确实是一只小鸟~~

为啥叫 "向上转型"?

在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 “向上转型” , 表示往父类的方向转.

向上转型发生的时机:

  • 直接赋值(子类被直接赋值给父类)

  • 方法传参(子类作为函数的参数)

  • 方法返回(子类作为返回值)

直接赋值的方式我们已经演示了. 另外两种方式和直接赋值没有本质区别.

3.1.1方法传参

public class Test {
    public static void main(String[] args) {
        Bird bird = new Bird("圆圆");
        feed(bird);
    }
    public static void feed(Animal animal) {
        animal.eat("谷子");
    }
}
// 执行结果
//圆圆正在吃谷子

此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例.

3.1.2方法返回

public class Test {
    public static void main(String[] args) {
        Animal animal = findMyAnimal();
    }
    public static Animal findMyAnimal() {
        Bird bird = new Bird("圆圆");
        return bird;
    }
}

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例.

3.2动态绑定

1、父类引用 引用 子类的对象

2、通过这个父类引用 调用父类 和 子类 同名的覆盖方法。

同名的覆盖 —> 重写(要加@Override在重写的结构体上 如果重写有问题则会报错):

a、方法名相同

b、参数列表相同(个数+类型)

c、返回值相同

d、父子类的情况下

若在重写的eat里改名字 则会报错

注意:

1、静态方法不能重写

2、子类的访问修饰限定符,要大于等于(保密性高于 例如private>protected>public)父类的访问修饰限定符。

3、private不能重写

4、被final修饰的方法,不能被重写

当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?

对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志.

// Animal.java
        public class Animal {
            protected String name;
            public Animal(String name) {
                this.name = name;
            }
            public void eat(String food) {
                System.out.println("我是一只小动物");
                System.out.println(this.name + "正在吃" + food);
            }}
// Bird.java 
        public class Bird extends Animal {
            public Bird(String name) {
                super(name);
            }
            public void eat(String food) {
                System.out.println("我是一只小鸟");
                System.out.println(this.name + "正在吃" + food);
            }
        }
// Test.java 
        public class Test {
            public static void main(String[] args) {
                Animal animal1 = new Animal("圆圆");
                animal1.eat("谷子");
                Animal animal2 = new Bird("扁扁");
                animal2.eat("谷子");
            }
        }
// 执行结果
//我是一只小动物
//圆圆正在吃谷子
//我是一只小鸟
//扁扁正在吃谷子

此时, 我们发现:

animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向Bird 类型的实例.

针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而animal2.eat() 实际调用了子类的方法.

因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.

3.3方法重写

针对刚才的 eat 方法来说:

子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override)

关于重写的注意事项

1.重写和重载完全不一样. 不要混淆(思考一下, 重载的规则是啥?)

2.普通方法可以重写, static 修饰的静态方法不能重写

3.重写中子类的方法的访问权限不能低于父类的方法访问权限.

4.重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).

方法权限示例: 将子类的 eat 改成 private

// Animal.java 
        public class Animal {
            public void eat(String food) { 
 ...
            }
        }
// Bird.java 
        public class Bird extends Animal {
            // 将子类的 eat 改成 private 
            private void eat(String food) { 
 ...
            }
        }
// 编译出错
        //Error:(8, 10) java: com.bit.Bird中的eat(java.lang.String)无法覆盖com.bit.Animal中的
        //eat(java.lang.String)
        //正在尝试分配更低的访问权限; 以前为public

另外, 针对重写的方法, 可以使用 @Override 注解来显式指定.

// Bird.java 
public class Bird extends Animal {
    @Override
    private void eat(String food) { 
         ...
   }
}

有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.

我们推荐在代码中进行重写方法时显式加上 @Override 注解.

关于注解的详细内容, 我们会在后面的章节再详细介绍.

小结: 重载和重写的区别.

体会动态绑定和方法重写

上面讲的动态绑定和方法重写是用的相同的代码示例.

事实上, 方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现. 两者本质上描述的是相同的事情, 只是侧重点不同.

3.4理解多态

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况.

代码示例: 打印多种形状

    class Shape {
        public void draw() {
            // 啥都不用干
        }
    }
    class Cycle extends Shape {
        @Override
        public void draw() { System.out.println("○");
        }
    }
    class Rect extends Shape {
        @Override
        public void draw() {
            System.out.println("□");
        }
    }
    class Flower extends Shape {
        @Override
        public void draw() {
            System.out.println("♣");
        }
    }
    /我是分割线// 
// Test.java 
    public class Test {
        public static void main(String[] args) {
            Shape shape1 = new Flower();
            Shape shape2 = new Cycle();
            Shape shape3 = new Rect();
            drawMap(shape1);
            drawMap(shape2);
            drawMap(shape3);
        }
        // 打印单个图形
        public static void drawShape(Shape shape) {
            shape.draw();
        }
    }

在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的.

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态

多态顾名思义, 就是 “一个引用, 能表现出多种不同形态”

举个具体的例子. 汤老湿家里养了两只鹦鹉(圆圆和扁扁)和一个小孩(核弹). 我媳妇管他们都叫 “儿子”. 这时候我对我媳妇说, “你去喂喂你儿子去”. 那么如果这里的 “儿子” 指的是鹦鹉, 我媳妇就要喂鸟粮; 如果这里的 “儿子” 指的是核弹, 我媳妇就要喂馒头.

那么如何确定这里的 “儿子” 具体指的是啥? 那就是根据我和媳妇对话之间的 “上下文”.

代码中的多态也是如此. 一个引用到底是指向父类对象, 还是某个子类对象(可能有多个), 也是要根据上下文的代码来确定.

PS: 大家可以根据汤老湿说话的语气推测一下在家里的家庭地位

使用多态的好处是什么?

1) 类调用者对类的使用成本进一步降低.

封装是让类的调用者不需要知道类的实现细节.

多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.

因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.

这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷.

2) 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else

例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:

public static void drawShapes() {
    Rect rect = new Rect();
    Cycle cycle = new Cycle();
    Flower flower = new Flower();
    String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
    for (String shape : shapes) {
        if (shape.equals("cycle")) {
            cycle.draw();
        } else if (shape.equals("rect")) {
            rect.draw();
        } else if (shape.equals("flower")) {
            flower.draw();
        }
    }
}

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.

public static void drawShapes() {
    // 我们创建了一个 Shape 对象的数组. 
    Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
            new Rect(), new Flower()};
    for (Shape shape : shapes) {
        shape.draw();
    }
}

使用多态前:

使用多态后:

由上面两个图的对比我们可以看出多态使我们代码看起来更加的高级,且大大的优化了我们的代码量。

什么叫 “圈复杂度” ?

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.

因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构 .

不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .

3) 可扩展能力更强.

如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.

class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("△");
    }
}

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.

而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.

3.5向下转型 - 子类引用父类对象

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途.

// Animal.java
public class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(String food) {
        System.out.println("我是一只小动物");
        System.out.println(this.name + "正在吃" + food);
    }
}
// Bird.java
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    public void eat(String food) {
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
    }
    public void fly() {
        System.out.println(this.name + "
                正在飞"); 
          } 
    }

接下来是我们熟悉的操作

Animal animal = new Bird("圆圆"); 
animal.eat("谷子");
// 执行结果
//圆圆正在吃谷子

接下来我们尝试让圆圆飞起来

animal.fly();
// 编译出错
//找不到 fly 方法

注意事项

编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.

虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.

对于 Animal animal = new Bird("圆圆") 这样的代码,

编译器检查有哪些方法存在, 看的是 Animal 这个类型

执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.

那么想实现刚才的效果, 就需要向下转型.

// (Bird) 表示强制类型转换
Bird bird = (Bird)animal; 
bird.fly();
// 执行结果
//圆圆正在飞

但是这样的向下转型有时是不太可靠的. 例如

Animal animal = new Cat("小猫");
Bird bird = (Bird)animal; 
bird.fly();
// 执行结果, 抛出异常
//Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird
//at Test.main(Test.java:35)

animal 本质上引用的是一个 Cat 对象 , 是不能转成 Bird 对象的. 运行时就会抛出异常.

所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换

Animal animal = new Cat("小猫"); 
if (animal instanceof Bird) {
        Bird bird = (Bird)animal;
        bird.fly();
    }

instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.

3.6super 关键字

前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字.

super 表示获取到父类实例的引用. 涉及到两种常见用法.

  1. 1.使用了 super 来调用父类的构造器(这个代码前面已经写过了)
public Bird(String name) {
    super(name);
}
  1. 2.使用 super 来调用父类的普通方法
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    @Override
    public void eat(String food) {
        // 修改代码, 让子调用父类的接口. 
        super.eat(food);
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
    }
}

在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了). 而加上 super 关键字, 才是调用父类的方法.

注意 super 和 this 功能有些相似, 但是还是要注意其中的区别.

3.7在构造方法中调用重写的方法(一个坑)

一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func

    class B {
        public B() {
            // do nothing
            func();
        }
        public void func() {
            System.out.println("B.func()");
        }
    }
    class D extends B {
        private int num = 1;
        @Override
        public void func() {
            System.out.println("D.func() " + num);
        }
    }
    public class Test {
        public static void main(String[] args) {
            D d = new D(); }
    }
// 执行结果
//D.func() 0

B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func

此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.

结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.

3.8总结

多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处.

另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系

C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.

Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.

Go 语言中没有 “继承” 这样的概念, 同样也能表示多态

无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式.

4.抽象类

4.1语法规则

在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstractmethod), 包含抽象方法的类我们称为 抽象类(abstract class).

abstract class Shape {
    abstract public void draw();
}
  • 在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体代码).
  • 对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类.

注意事项

1)抽象类不能直接实例化

Shape shape = new Shape();
// 编译出错
//Error:(30, 23) java: Shape是抽象的; 无法实例化

2)抽象方法不能是 private 的

abstract class Shape {
    abstract private void draw();
}
// 编译出错
//Error:(4, 27) java: 非法的修饰符组合: abstract和private

3)抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用

    abstract class Shape {
        abstract public void draw();
        void func() {
            System.out.println("func");
        }
    }
    class Rect extends Shape { 
 ...
    }
    public class Test {
        public static void main(String[] args) {
            Shape shape = new Rect();
            shape.func();
        }
    }
// 执行结果
//func

4)包含抽象方法的类,叫抽象类

5)什么是抽象方法,一个没有具体实现的方法,被abstract修饰。

6)因为抽象类不能被实例化,所以这个抽象类,其实只能被继承

7)抽象类当中,也可以包含和普通类一样的成员和方法

8)一个普通类,继承了一个抽象类,那么这个普通类当中,需要重写这个抽象的所有的抽象方法。

9)抽象类最大的作用就是为了被继承

10)一个抽象类A,如果继承了一个抽象类B,那么这个抽象类A,可以不实现抽象父类B的抽象方法。

11)结合第十点,当A类 再次被一个普通类继承后,那么A和B这两个抽象类当中的抽象方法,必须被重写。

4.2抽象类的作用

抽象类存在的最大意义就是为了被继承.

抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.

有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?

确实如此. 但是使用抽象类相当于多了一重编译器的校验

使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.

很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.

充分利用编译器的校验, 在实际开发中是非常有意义的.

5.接口

因为我们如果要定义一个鸭子它能飞能跑还能游泳,如果继承类的话就只能继承一个,且不是所有的动物都能飞、跑和游泳,但是如果我们继承接口的话,我们可以继承三个不同的接口(飞,跑,游泳)来实现我们不同的功能,由此便达到了我们的目的,接口便也诞生了!

1、使用interface来修饰的。interfece IA {}

2、接口当中的普通方法,不能有具体的实现。非要实现,只能通过关键字default来修饰这个方法。

3、接口当中,可以有static的方法。

4、里面的所有的方法都是public的。

5、抽象方法默认是public abstract的。

6、接口是不可以被通过关键字new来实例化的。

7、类和接口之间的关系是通过implements来进行实现的。

8、当一个类实现了一个接口,就必须要重写接口当中的抽象方法。

9、接口当中的成员变量,默认是public static final修饰的。

10、当一个类实现一个接口之后,重写这个方法的时候,这个方法前面必须加上public

11、一个类可以通过关键字extends继承一个抽象类或者一个普通类,但是只能继承一个类,同时也可以通过implements实现多个接口,接口之间使用逗号隔开就好。

12、接口和接口之间 可以使用extends来 操作他们的关系,此时,这里面意为:拓展。一个接口 通过 extends来 拓展另一个接口的功能。此时当一个类D 通过implements实现这个接口B的时候,此时重写的方法不仅仅是B的抽象方法,还有他从C接口,拓展来的功能[方法]。

接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含静态常量.

5.1语法规则

在刚才的打印图形的示例中, 我们的父类 Shape 并没有包含别的非抽象方法, 也可以设计成一个接口

interface IShape {
    void draw();
}
class Cycle implements IShape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}
public class Test {
    public static void main(String[] args) {
        IShape shape = new Rect();
        shape.draw();
    }
}

使用 interface 定义一个接口

接口中的方法一定是抽象方法, 因此可以省略 abstract

接口中的方法一定是 public, 因此可以省略 public

Cycle 使用 implements 继承接口. 此时表达的含义不再是 “扩展”, 而是 “实现”

在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.

接口不能单独被实例化.

扩展(extends) vs 实现(implements)

扩展指的是当前已经有一定的功能了, 进一步扩充功能.

实现指的是当前啥都没有, 需要从头构造出来.

接口中只能包含抽象方法. 对于字段来说 , 接口中只能包含静态常量(final static).interface

interface IShape {
    void draw();
    public static final int num = 10;
}

其中的 public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量.

提示:

我们创建接口的时候, 接口的命名一般以大写字母 I 开头.

接口的命名一般使用 “形容词” 词性的单词.

阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.

一个错误的代码:

interface IShape {
    abstract void draw() ; // 即便不写public,也是public 
}
class Rect implements IShape {
    void draw() {
        System.out.println("□") ; //权限更加严格了,所以无法覆写。
    }
}

接口可以不加public(因为会默认为public),但是子类继承接口后必须要带上public,因为如果子类权限低于接口则idea会报错。

5.2实现多个接口

有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.

然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.现在我们通过类来表示一组动物.

class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
}

另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”.

interface IFlying {
    void fly();
}
interface IRunning {
    void run();
}
interface ISwimming {
    void swim();
}

接下来我们创建几个具体的动物

猫, 是会跑的.

class Cat extends Animal implements IRunning {
    public Cat(String name) {
        super(name);
    }
    @Override
    public void run() {
        System.out.println(this.name + "正在用四条腿跑");
    }
}

鱼, 是会游的.

class Fish extends Animal implements ISwimming {
    public Fish(String name) {
        super(name);
    }
    @Override
    public void swim() {
        System.out.println(this.name + "正在用尾巴游泳");
    }
}

青蛙, 既能跑, 又能游(两栖动物)

class Frog extends Animal implements IRunning, ISwimming {
    public Frog(String name) {
        super(name);
    }
    @Override
    public void run() {
        System.out.println(this.name + "正在往前跳");
    }
    @Override
    public void swim() {
        System.out.println(this.name + "正在蹬腿游泳");
    }
}

提示, IDEA 中使用 ctrl + i 快速实现接口

还有一种神奇的动物, 水陆空三栖, 叫做 “鸭子”

class Duck extends Animal implements IRunning, ISwimming, IFlying {
    public Duck(String name) {
        super(name);
    }
    @Override
    public void fly() {
        System.out.println(this.name + "正在用翅膀飞");
    }
    @Override
    public void run() {
        System.out.println(this.name + "正在用两条腿跑");
    }
    @Override
    public void swim() {
        System.out.println(this.name + "正在漂在水上");
    }
}

上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.

继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性

猫是一种动物, 具有会跑的特性.

青蛙也是一种动物, 既能跑, 也能游泳

鸭子也是一种动物, 既能跑, 也能游, 还能飞

这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力.

例如, 现在实现一个方法, 叫 “散步”

public static void walk(IRunning running) {
    System.out.println("我带着伙伴去散步");
    running.run();
}

在这个 walk 方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行

Cat cat = new Cat("小猫");
walk(cat);
Frog frog = new Frog("小青蛙");
walk(frog);
// 执行结果
//我带着伙伴去散步
//小猫正在用四条腿跑
//我带着伙伴去散步
//小青蛙正在往前跳

甚至参数可以不是 “动物”, 只要会跑!

    class Robot implements IRunning {
        private String name;
        public Robot(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            System.out.println(this.name + "正在用轮子跑");
        }
    }
    Robot robot = new Robot("机器人");
    walk(robot);
// 执行结果
//机器人正在用轮子跑

5.3接口使用实例

刚才的例子比较抽象, 我们再来一个更能实际的例子.

给对象数组排序

给定一个学生类

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 方法

class Student implements Comparable {
    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 + "]";
    }
    @Override
    public int compareTo(Object o) {
        Student s = (Student)o;
        if (this.score > s.score) {
            return -1;
        } else if (this.score < s.score) {
            return 1;
        } else {
            return 0;
        }
    }
}

在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.然后比较当前对象和参数对象的大小关系(按分数来算).

如果当前对象应排在参数对象之前, 返回小于 0 的数字;

如果当前对象应排在参数对象之后, 返回大于 0 的数字;

如果当前对象和参数对象不分先后, 返回 0;

再次执行程序, 结果就符合预期了.

// 执行结果
//[[王五:97], [李四:96], [张三:95], [赵六:92]]

注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则.

为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序)

public static void sort(Comparable[] array) {
    for (int bound = 0; bound < array.length; bound++) {
        for (int cur = array.length - 1; cur > bound; cur--) {
            if (array[cur - 1].compareTo(array[cur]) > 0) {
                // 说明顺序不符合要求, 交换两个变量的位置
                Comparable tmp = array[cur - 1];
                array[cur - 1] = array[cur];
                array[cur] = tmp;
            }
        }
    }
}

再次执行代码

sort(students);
System.out.println(Arrays.toString(students));
// 执行结果
//[[王五:97], [李四:96], [张三:95], [赵六:92]]

5.4接口间的继承

接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.

        interface IRunning {
            void run();
        }
        interface ISwimming {
            void swim();
        }
// 两栖的动物, 既能跑, 也能游
        interface IAmphibious extends IRunning, ISwimming {
        }
        class Frog implements IAmphibious { 
 ...
        }

通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法.

接口间的继承相当于把多个接口合并在一起.

5.5Clonable 接口和深拷贝

Java 中内置了一些很有用的接口, Clonable 就是其中之一.

Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛 CloneNotSupportedException 异常.

        class Animal implements Cloneable {
            private String name;
            @Override
            public Animal clone() {
                Animal o = null;
                try {
                    o = (Animal)super.clone();
                } catch (CloneNotSupportedException e) { e.printStackTrace();
                }
                return o;
            }
        }
        public class Test {
            public static void main(String[] args) {
                Animal animal = new Animal();
                Animal animal2 = animal.clone();
                System.out.println(animal == animal2);
            }
        }
// 输出结果
// false

浅拷贝 VS 深拷贝

Cloneable 拷贝出的对象是一份 “浅拷贝”

观察以下代码:

        public class Test {
            static class A implements Cloneable {
                public int num = 0;
                @Override
                public A clone() throws CloneNotSupportedException {
                    return (A)super.clone();
                }
            }
            static class B implements Cloneable {
                public A a = new A();
                @Override
                public B clone() throws CloneNotSupportedException {
                    return (B)super.clone();
                }
            }
            public static void main(String[] args) throws CloneNotSupportedException {
                B b = new B();
                B b2 = b.clone();
                b.a.num = 10;
                System.out.println(b2.a.num);
            }
        }
// 执行结果
//10

通过 clone 拷贝出的 b 对象只是拷贝了 b 自身, 而没有拷贝内部包含的 a 对象. 此时 b 和 b2 中包含的 a 引用仍然是指向同一个对象. 此时修改一边, 另一边也会发生改变.

未来学到序列化的时候, 会告诉大家如何进行深拷贝.

总结

抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别(重要!!! 常见面试题).

核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法.

如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口.

class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
}

再次提醒:

抽象类存在的意义是为了让编译器更好的校验, 像 Animal 这样的类我们并不会直接使用, 而是使用它的子类. 万一不小心创建了 Animal 的实例, 编译器会及时提醒我们

感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手点一点赞,你的每一次鼓励都是作者创作的动力哦!😘

目录
相关文章
|
3月前
|
Java 开发者
在Java面向对象编程的广阔海洋中,多态犹如一股深邃的潜流,它推动着代码从单一走向多元,从僵化迈向灵活。
在Java面向对象编程的广阔海洋中,多态犹如一股深邃的潜流,它推动着代码从单一走向多元,从僵化迈向灵活。
38 7
|
3月前
|
Java 开发者
那些年,我们一同踏入Java编程的大门,多态,这个充满魔法的名字,曾无数次点亮我们探索面向对象编程的热情。
那些年,我们一同踏入Java编程的大门,多态,这个充满魔法的名字,曾无数次点亮我们探索面向对象编程的热情。
46 5
|
3月前
|
Java 程序员
Java中的继承和多态:理解面向对象编程的核心概念
【8月更文挑战第22天】在Java的世界中,继承和多态不仅仅是编程技巧,它们是构建可维护、可扩展软件架构的基石。通过本文,我们将深入探讨这两个概念,并揭示它们如何共同作用于面向对象编程(OOP)的实践之中。你将了解继承如何简化代码重用,以及多态如何为程序提供灵活性和扩展性。让我们启程,探索Java语言中这些强大特性的秘密。
|
5月前
|
Java
Java面向对象编程新篇章:多态,你准备好了吗?
【6月更文挑战第17天】Java的多态性是面向对象编程的核心,它允许通过统一的接口处理不同类型的对象。例如,在一个虚拟宠物游戏中,抽象类`Pet`定义了`speak()`方法,猫、狗和鹦鹉等子类各自重写此方法以实现独特叫声。在`main`方法中,使用`Pet`类型的引用创建子类对象并调用`speak()`,多态机制确保调用实际对象的方法,实现代码的灵活性和可扩展性。通过多态,我们能以更低的耦合度和更高的复用性编写更优雅的代码。
36 3
|
5月前
|
Java
Java 面向对象编程:父类与子类的“传承”与“创新”之路
【6月更文挑战第16天】Java 中的父类与子类展示了面向对象的“传承”与“创新”。子类`Dog`继承`Animal`,获取其属性和方法如`name`和`makeSound`。子类通过`@Override`增强`makeSound`,显示多态性。设计父类时应考虑普遍性,子类创新专注自身特性,遵循继承最佳实践,利用复用提升效率,构建可维护的软件系统。
150 57
|
3月前
|
存储 前端开发 JavaScript
【前端学java】面向对象编程基础-类的使用(4)
【8月更文挑战第9天】面向对象编程基础-类的使用
19 0
【前端学java】面向对象编程基础-类的使用(4)
|
3月前
|
Java 程序员 开发者
Java的面向对象编程:从基础到深入
【8月更文挑战第21天】在本文中,我们将探讨Java的面向对象编程(OOP)的核心概念,包括类、对象、继承、多态和封装。我们将通过实例和比喻,以通俗易懂的方式,深入理解这些概念的内涵和意义,以及它们如何影响我们的编程思维和实践。无论你是初学者还是有经验的开发者,这篇文章都将帮助你更深入地理解Java的OOP,并启发你思考如何在你的项目中应用这些概念。
|
4月前
|
Java API 项目管理
Java中的函数式编程与传统面向对象编程对比
Java中的函数式编程与传统面向对象编程对比
|
5月前
|
Java 安全 索引
滚雪球学Java(48):面向对象编程中的StringBuffer类详解
【6月更文挑战第2天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
59 5
滚雪球学Java(48):面向对象编程中的StringBuffer类详解
|
5月前
|
存储 安全 Java
深入探讨 Java 封装机制:为何它是面向对象编程的核心?
【6月更文挑战第16天】Java的封装是OOP核心,它将数据和操作数据的方法打包在类中,隐藏实现细节并提供公共接口。例如,`Student`类封装了私有属性`name`和`age`,通过`get/set`方法安全访问。封装提升代码稳定性、可维护性和复用性,防止外部直接修改导致的错误,确保数据安全。它是面向对象编程优于传统编程的关键,促进高效、可靠的开发。
58 7
下一篇
无影云桌面