Java(包,继承,组合,多态,抽象类,接口)复习专用

今天,带大家一起复习这一节内容

包 (package) 是组织类的一种方式.
使用包的主要目的是保证类的唯一性.
例如, 你在代码中写了一个 Test 类. 然后你的同事也可能写一个 Test 类. 如果出现两个同名的类, 就会冲突, 导致代码不能编译通过

导入包中的类

Java 中已经提供了很多现成的类供我们使用. 例如

public class Test {
    public static void main(String[] args) {
        java.util.Date date=new java.util.Date();        // 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}

在这里插入图片描述

可以使用 java.util.Date 这种方式引入 java.util 这个包中的 Date 类.
但是这种写法比较麻烦一些, 可以使用 import 语句导入包

import java.util.Date;
public class Test {
    public static void main(String[] args) {
        Date date = new Date();
        // 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
   }
}

如果需要使用 java.util 中的其他类, 可以使用 import java.util.*

import java.util.*;
public class Test {
    public static void main(String[] args) {
        Date date = new Date();
        // 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
   }
}

但是我们更建议显式的指定要导入的类名. 否则还是容易出现冲突的情况

import java.util.*;
import java.sql.*;
public class TestDemo {
 public static void main(String[] args) {
 // util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错
          Date date = new Date();
          System.out.println(date.getTime());
  }
}

在这里插入图片描述

在这种情况下需要使用完整的类名

import java.util.*;
import java.sql.*;
public class Test {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();
        System.out.println(date.getTime());
   }
}

注意事项: import 和 C++ 的 #include 差别很大. C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要…
import 只是为了写代码的时候更方便. import 更类似于 C++ 的 namespace和using

静态导入

使用 import static 可以导入包中的静态的方法和字段

import static java.lang.System.*;
public class Test {
    public static void main(String[] args) {
        out.println("hello");
   }
}

在这里插入图片描述

使用这种方式可以更方便的写一些代码, 例如

import static java.lang.Math.*;
public class Test {
    public static void main(String[] args) {
        double x = 30;
        double y = 40;
        // 静态导入的方式写起来更方便一些. 
        // double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        double result = sqrt(pow(x, 2) + pow(y, 2));
        System.out.println(result);
   }
}

在这里插入图片描述

将类放到包中

基本规则

1、在文件的最上方加上一个 package 语句指定该代码在哪个包中.
2、包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒(com.bit.demo1 ).
3、包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码.
4、如果一个类没有 package 语句, 则该类被放到一个默认包中.

操作步骤

  1. 在 IDEA 中先新建一个包: 右键 src -> 新建 -> 包
    在这里插入图片描述
  1. 在弹出的对话框中输入包名.例如 demo1
    在这里插入图片描述

3.在包中创建类, 右键包名 -> 新建 -> 类, 然后输入类名即可.
在这里插入图片描述

4.同时我们也看到了, 在新创建的 Test.java 文件的最上方, 就出现了一个 package 语句
在这里插入图片描述

这里我们要注意一下,包名必须要小写

包的访问权限控制

我们已经了解了类中的 public 和 private. private 中的成员只能被类的内部使用.
如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使
用.
下面的代码给了一个示例. Demo1 和 Demo2 是同一个包中, Test 是其他包中.

TestDemo1.java

package demo1;
public class TestDemo1 {
    int value = 0; 
    }

TestDemo2.java

package demo1; 
public class TestDemo2 { 
 public static void main(String[] args) { 
 TestDemo1 demo = new TestDemo1(); 
 System.out.println(demo.value); 
 } 
}

在这里插入图片描述

这里可以访问到变量
在这里插入图片描述

这里我们就 无法从外部程序包中对其进行访问

常见的系统包

1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
2. java.lang.reflect:java 反射编程包;
3. java.net:进行网络编程开发包。
4. java.sql:进行数据库开发的支持包。
5. java.util:是java提供的工具程序包。(集合类等) 非常重要
6. java.io:I/O编程开发包

继承

背景

代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法).
有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联.
例如, 设计一个类表示动物

注意, 我们可以给每个类创建一个单独的 java 文件. 类名必须和 .java 文件名匹配(大小写敏感)

// Animal.java 
public class Animal { 
 public String name; 
 
 public Animal(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃!" + food); 
 } 
} 


// Cat.java 
class Cat { 
 public String name; 
 
 public Cat(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃!" + food); 
 } 
} 


// Bird.java 
class Bird { 
 public String name; 
 
 public Bird(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃!" + food); 
 } 
 
 public void fly() { 
 System.out.println(this.name + "正在飞 !"); 
 } 
}

这个代码我们发现其中存在了大量的冗余代码.
仔细分析, 我们发现 Animal 和 Cat 以及 Bird 这几个类中存在一定的关联关系:
这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的.
这三个类都具备一个相同的 name 属性, 而且意义是完全一样的.
从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义).
此时我们就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码重用的效果.
此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类
和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果

语法规则

基本语法

class 子类 extends 父类 { 
}

使用 extends 指定父类.
Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
子类会继承父类的所有 public 的字段和方法.
对于父类的 private 的字段和方法, 子类中是无法访问的.
子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用.

对于上面的代码, 可以使用继承进行改进. 此时我们让 Cat 和 Bird 继承自 Animal 类, 那么 Cat 在定义的时候就不必再写 name 字段和 eat 方法

class Animal {
 public String name;
 public Animal(String name) {
  this.name = name;
 }
 public void eat(String food) {
  System.out.println(this.name + "正在吃" + food);
 }
}
class Cat extends Animal {
 public Cat(String name) {
  // 使用 super 调用父类的构造方法.
  super(name);
 }
}
class Bird extends Animal {
 public Bird(String name) {
  super(name);
 }
 public void fly() {
  System.out.println(this.name + "正在飞 ");
 }
}
public class Test {
 public static void main(String[] args) {
  Cat cat = new Cat("bibi");
  cat.eat("猫粮");
  Bird bird = new Bird("kiki");
  bird.fly();
 }
}

在这里插入图片描述

如果我们把 name 改成 private, 那么此时子类就不能访问了
在这里插入图片描述在这里插入图片描述
封装有什么好处呢?
他不必要公开数据成员和方法,使我们的代码更加安全

protected 关键字

有什么两全其美的办法吗?
就是使用protected 关键字。
对于类的调用者来说, protected 修饰的字段和方法是不能访问的
对于类的 子类同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的
在这里插入图片描述

不在同一个包中,就不能被访问

小结:Java 中对于字段和方法共有四种访问权限
private: 类内部能访问, 类外部不能访问
默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.
protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.
public : 类内部和类的调用者都能访问

在这里插入图片描述

什么时候下用哪一种呢?

我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public.
另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)

更复杂的继承关系

刚才我们的例子中, 只涉及到 Animal, Cat 和 Bird 三种类. 但是如果情况更复杂一些呢?
针对 Cat 这种情况, 我们可能还需要表示更多种类的猫~
在这里插入图片描述

这个时候使用继承方式来表示, 就会涉及到更复杂的体系

// Animal.java 
public Animal { 
 ... 
} 
// Cat.java 
public Cat extends Animal { 
 ... 
} 
// ChineseGardenCat.java 
public ChineseGardenCat extends Cat { 
 ... 
} 
// OrangeCat.java 
public Orange extends ChineseGardenCat { 
 ... 
} 
......

如刚才这样的继承方式称为多层继承, 即子类还可以进一步的再派生出新的子类.

时刻牢记, 我们写的类是现实事物的抽象. 而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到一系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会更加复杂.
但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了.
如果想从语法上进行限制继承, 就可以使用 final 关键字

final 关键字

曾经我们学习过 final 关键字, 修饰一个变量或者字段的时候, 表示 常量 (不能修改).

final int a = 10; 
a = 20; // 编译出错

final 关键字也能修饰类, 此时表示被修饰的类就不能被继承

final public class Animal { 
 ... 
} 
public class Bird extends Animal { 
 ... 
}  //编译出错

final 关键字的功能是 限制 类被继承

“限制” 这件事情意味着 “不灵活”. 在编程中, 灵活往往不见得是一件好事. 灵活可能意味着更容易出错.
是用 final 修饰的类被继承的时候, 就会编译报错, 此时就可以提示我们这样的继承是有悖这个类设计的初衷的
在这里插入图片描述

我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承

组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.

例如表示一个学校

public class Student { 
 ... 
} 
public class Teacher { 
 ... 
} 
public class School { 
 public Student[] students; 
 public Teacher[] teachers; 
}

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.
这是我们设计类的一种常用方式之一.
组合表示 has - a 语义
在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师.
继承表示 is - a 语义
在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物.
大家要注意体会两种语义的区别

多态

向上转型

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

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

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

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

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

此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为向上转型.
什么情况下会发生向上转型?
直接赋值
方法传参
方法返回

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

方法传参

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 (父类) 的实例

方法返回

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 的实例

向下转型

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

// 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. 这时再进行向下转型就比较安全了

动态绑定

什么叫动态绑定?
1.父类引用 引用了子类的对象
2.通过这个父类引用 调用 父类 和子类同名的覆盖方法

重写

同名的覆盖方法就是叫重写:
1.方法名相同
2.参数列表相同(个数+类型)
3.返回值相同
4.父子类的情况下

注意:
1.方法不可以是static
2.子类的访问修饰限定符要大于等于父类的
3.private方法 不能被重写
4.被final修饰的方法不能被重写

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

class Shape{
    public void draw(){
        System.out.println("Shape::draw()");
    }
}

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


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

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


public class TestDemo {

    public static void drawMap(Shape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        Rect rect = new Rect();
        Flower flower = new Flower();
        Triangle triangle = new Triangle();
        Shape[] shapes = {triangle,rect,triangle,rect,flower};
        for (Shape shape:shapes) {
            shape.draw();
        }
    }

    public static void main2(String[] args) {
        Shape shape = new Rect();
        drawMap(shape);
        drawMap(new Flower());
        drawMap(new Triangle());
    }


    public static void main1(String[] args) {
//        Rect rect = new Rect();
//        rect.draw();
        Shape shape1 = new Rect();
        shape1.draw();
        Shape shape2 = new Flower();
        shape2.draw();

    }
}

有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 drew), 那么此时编译器就会发…

现父类中没有 drew 方法, 就会编译报错, 提示无法构成重写…

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

小结: 重载和重写的区别
在这里插入图片描述

super 关键字

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

public Bird(String name) { 
 super(name); 
}

使用 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 关键字, 才是调用父类的方法…

在这里,我们要注意this和super的区别
在这里插入图片描述

抽象类

抽象类:

1、包含抽象方法的类,叫做抽象类。
2、什么是抽象方法,-个没有具体实现的方法,被abstract修饰。
3、抽象类是不可以被实例化的。(new)
4、因为不能被实例化,所以,这个抽象类,其实只能被继承。
5、抽象类当中,也可以包含,和普通类一样的成员和方法。
6、一个普通类,继承了-个抽象类,那么这个普通类当中,需要重写这个抽象类的所有的抽象方法。
7、抽象类最大的作用,就是为了被继承。
8、一个抽象类A,如果继承了- -个抽象类B,那么这个抽象类A,可以不实现抽象父类B的抽象方法。
9、结合第8点,当A类再次被一个普通类继承后,那么A和B这两个抽象类当中的抽象方法,必须被重写。
10、抽象类不能被final修饰,抽象方法也不可以被final|修饰。

抽象类的作用

抽象类存在的最大意义就是为了被继承.
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.

普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?

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

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

abstract  class Shape{
    public abstract void draw();//抽象方法
    public void func(){
        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("❀");
    }
}

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

class Cycle extends Shape{
    @Override
    public void draw() {
        System.out.println("○");
        super.func();
    }
}

abstract  class A{
    public abstract void func();
    String name;

    public A(String name){
        this.name = name;
    }
    public void func1(){
        System.out.println(name+"hahha");
    }
}

class B extends A{
    public B(String name){
        super(name);
    }
    public void func(){

    }
}
public class TestDemo {

    public static void drewMap(Shape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        Shape shape = new Rect();
        Shape shape1 = new Flower();
        Shape shape2 = new Triangle();
        Shape shape3 = new Cycle();
        drewMap(shape1);
        Cycle cycle = new Cycle();
        drewMap(cycle);
    }


}

接口

使用 interface 定义一个接口…
接口中的方法一定是抽象方法, 因此可以省略 abstract
接口中的方法一定是 public, 因此可以省略 public

Cycle 使用 implements 继承接口. 此时表达的含义不再是 “扩展”, 而是 “实现”
在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.
接口不能单独被实例化
扩展(extends) vs 实现(implements)

扩展指的是当前已经有一定的功能了, 进一步扩充功能.
实现指的是当前啥都没有, 需要从头构造出来
接口中只能包含抽象方法. 对于字段来说, 接口中只能包含静态常量(final static).

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

提示:
1. 我们创建接口的时候, 接口的命名一般以大写字母 I 开头.
2. 接口的命名一般使用 “形容词” 词性的单词.
3. 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性

一个错误的代码

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

在这里插入图片描述

实现多个接口

有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.
然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.
提示: IDEA 中使用 ctrl + i 快速实现接口

接口间的继承

接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字…
通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法.
接口间的继承相当于把多个接口合并在一起

//一类动物
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 Bird extends Animal implements IFlying{
    public Bird(String name){
    super(name);
    }

    @Override
    public void fly() {
        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+"正在游泳!");
    }
}

//鸭子飞、跑、游
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+"正在游泳!");
    }
}
public class TestDemo4 {

    public static void runFunc(IRunning iRunning){
        iRunning.run();
    }

    public  static void flyFunc(IFlying iFlying){
        iFlying.fly();
    }

    public static void swimFunc(ISwimming iSwimming){
        iSwimming.swim();
    }

    public static void main(String[] args) {
        runFunc(new duck("鸭子"));
        runFunc(new Frog("青蛙"));
        flyFunc(new duck("鸭子"));
        flyFunc(new Bird("小鸟"));
        swimFunc(new duck("鸭子"));
        swimFunc(new Frog("青蛙"));
    }

}

上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性

在这里插入图片描述

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

接口使用实例

刚才的例子比较抽象, 我们再来一个更能实际的例子.
给对象数组排序

给定一个学生类:

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

Comparable

class Student implements Comparable<Student>{
    public int age;
    public String name;
    public double score;

    public Student(int age, String name, double score) {
        this.age = age;
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", score=" + score +
                '}';
    }

    //谁调用这个方法,谁就是this
    @Override
    public int compareTo(Student o) {
        /*if(this.age > o.age){
            return 1;
        }else if(this.age == o.age){
            return 0;
        }else{
            return -1;
        }*/
       // return this.age - o.age;  //从小到大排序
       // return o.age - this.age;    // 从大到小排序
        return this.name.compareTo(o.name);
    }
}

public class TestDemo{

    public static void main2(String[] args) {
        Student student1 = new Student(12,"bbt",98.2);
        Student student2 = new Student(6,"aac",88.2);

        System.out.println(student1.compareTo(student2));
    }


    public static void main(String[] args) {
        Student[] students = new Student[3];
       students[0] = new Student(12,"bbt",98.2);
       students[1] = new Student(6,"aac",88.2);
       students[2] = new Student(18,"cca",68.2);
        System.out.println(Arrays.toString(students));

        Arrays.sort(students);

        System.out.println(Arrays.toString(students));
    }
}

如果 自定义的数据类型 进行比较大小 一定要实现可以比较的接口
这个接口 有一个很大的缺陷: 对类的侵入性非常强,一旦写好了,不敢轻易改动。

在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.
然后比较当前对象和参数对象的大小关系(按分数来算).
如果当前对象应排在参数对象之前, 返回小于 0 的数字;
如果当前对象应排在参数对象之后, 返回大于 0 的数字;
如果当前对象和参数对象不分先后, 返回0
再次执行程序, 结果就符合预期了

Comparator

class Student {
    public int age;
    public String name;
    public double score;

    public Student(int age, String name, double score) {
        this.age = age;
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", score=" + score +
                '}';
    }

    //谁调用这个方法,谁就是this
    // @Override
    //public int compareTo(Student o) {
        /*if(this.age > o.age){
            return 1;
        }else if(this.age == o.age){
            return 0;
        }else{
            return -1;
        }*/
    // return this.age - o.age;  //从小到大排序
    // return o.age - this.age;    // 从大到小排序
//      return this.name.compareTo(o.name);
//    }
//}
}

    class AgeComparator implements Comparator<Student> {

        public int compare(Student o1, Student o2) {
            return o1.age - o2.age;
        }
    }

    class ScoreComparator implements Comparator<Student>{
        @Override
        public int compare(Student o1, Student o2) {
            return (int)(o1.score - o2.score);
        }
    }

    class NameComparator implements Comparator<Student>{
        @Override
        public int compare(Student o1, Student o2) {
            return o1.name.compareTo(o2.name);
        }
    }

public class TestDemo{

    public static void main2(String[] args) {
        Student student1 = new Student(12,"bbt",98.2);
        Student student2 = new Student(6,"aac",88.2);

        //System.out.println(student1.compareTo(student2));
       AgeComparator ageComparator = new AgeComparator();
        System.out.println(ageComparator.compare(student1,student2));
    }


    public static void main(String[] args) {
        Student[] students = new Student[3];
       students[0] = new Student(12,"bbt",98.2);
       students[1] = new Student(6,"aac",88.2);
       students[2] = new Student(18,"cca",68.2);
        System.out.println(Arrays.toString(students));

        AgeComparator ageComparator = new AgeComparator();
        Arrays.sort(students,ageComparator);
        System.out.println(Arrays.toString(students));

        ScoreComparator scoreComparator = new ScoreComparator();
        Arrays.sort(students,scoreComparator);
        System.out.println(Arrays.toString(students));

        NameComparator nameComparator = new NameComparator();
        Arrays.sort(students,nameComparator);
        System.out.println(Arrays.toString(students));

    }
}

在这里插入图片描述这个接口就比较灵活,对类的侵入性非常弱,用哪一个接口更好,取决于具体在什么业务上但还是比较推荐比较器(Comparator)
为了进一步加深对接口的理解, 我们可以尝试自己实现一个 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; 
 } 
 } 
 } 
}

总结

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

抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不
能包含普通方法, 子类必须重写所有的抽象方法.
如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口

在这里插入图片描述

好了,今天的复习就到此结束了,感谢大家的支持,你们的鼓励是我学习最大的动力,加油!祝我们一起进步!!

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

粉色的志明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值