Java-继承与多态

一.包

1.什么是包

包 (package) 是组织类的一种方式,使用包的主要目的是保证类的唯一性。

2.导入包中的类

Java中已经提供了很多现成的类供我们使用,比如我们可以使用 java.util.Date 这种方式引入 java.util 这个包中的 Date 类。代码如下:

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

但是这种写法往往比较麻烦,所以我们可以使用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());
   }
}

注意:Java是用到包中的哪个类就导入哪个类

但是 我们更建议显示的指定要导入的类名,否则还是容易出现冲突的情况。例如:

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

这种情况下我们就需要完整的包名:

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 java.util.*导入包下的所有类,Java是用到哪个类再去拿哪个类,而不是像include一样导入所有的文件

3.静态导入

使用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);
   }
}

4.将类放入包中

基本规则:

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

5.包的访问权限控制

我们已经了解了类中的 public 和 private, private 中的成员只能被类的内部使用

如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用

下面的代码给了一个示例。 Demo1 和 Demo2 是同一个包中, Test 是其他包中:
Demo1.java

package com.bit.demo;
public class Demo1 {
    
    
    int value = 0; 
    }

Demo2.java

package com.bit.demo; 
public class Demo2 {
    
     
 	public static void Main(String[] args) {
    
     
 		Demo1 demo = new Demo1(); 
 		System.out.println(demo.value); 
 	} 
} 
// 执行结果, 能够访问到 value 变量
10

Test.java

import com.bit.demo.Demo1; 
public class Test {
    
     
 public static void main(String[] args) {
    
     
 Demo1 demo = new Demo1(); 
 System.out.println(demo.value); 
 } 
} 
// 编译出错
Error:(6, 32) java: value在com.bit.demo.Demo1中不是公共的; 无法从外部程序包中对其进行访问

6.常见的系统包

  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编程开发包

二.继承

1.什么是继承

简单来说,继承的意义就是实现代码的复用

代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法)

有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联

例如, 设计一个类表示动物:

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

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

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 + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
}

这个代码我们发现其中存在了大量的冗余代码

仔细分析, 我们发现 AnimalCat 以及 Bird 这几个类中存在一定的关联关系:

  • 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的
  • 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的
  • 这三个类都具备一个相同的 name 属性, 而且意义是完全一样的
  • 从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义)

此时我们就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码重用的效果

2.继承的语法规则

基本语法:

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

这时候,我们再把上面的代码改一下,通过 extends 关键字实现继承,将 CatBird 继承 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("小黑"); 
 		cat.eat("猫粮"); 
 		Bird bird = new Bird("圆圆"); 
 		bird.fly(); 
 	} 
}

像Animal这种被继承的类,我们称为 父类、基类 或者 超类,对于像 Cat 和 Bird 这样的类,我们称为 子类 或者 派生类。和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果

这时候,如果我们把 name 改成 private, 那么此时子类就不能访问了:

 public Bird(String name) {
    
     
 	super(name); 
 } 
 public void fly() {
    
     
 	System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
} 
// 编译出错
Error:(19, 32) java: name 在 Animal 中是 private 访问控制

3.protected关键字

刚才我们发现, 如果把字段设为 private, 子类不能访问。但是设成 public, 又违背了我们 “封装” 的初衷。两全其美的办法就是 protected 关键字

  • 对于类的调用者来说, protected 修饰的字段和方法是不能访问的
  • 对于类的 子类同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的
// Animal.java 
public class Animal {
    
     
 	protected String name; 
 	public Animal(String name) {
    
     
 		this.name = name; 
 } 
 public void eat(String food) {
    
     
 	System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Bird.java 
public class Bird extends Animal {
    
     
 	public Bird(String name) {
    
     
 		super(name); 
 	} 
 	public void fly() {
    
     
 // 对于父类的 protected 字段, 子类可以正确访问
 		System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 	} 
} 
// Test.java 和 Animal.java 不在同一个 包 之中了 
public class Test {
    
     
 	public static void main(String[] args) {
    
     
 		Animal animal = new Animal("小动物"); 
 		System.out.println(animal.name); // 此时编译出错, 无法访问 name 
 	} 
}

小结:

Java 中对于字段和方法共有四种访问权限:

  1. private: 类内部能访问, 类外部不能访问
  2. 默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问
  3. protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问
  4. public : 类内部和类的调用者都能访问
    在这里插入图片描述
    什么时候下用哪一种呢?
    我们希望类要尽量做到 封装, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者。
    因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用 public 。
    另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public。不过这种方式属于是对访问权限的滥用,还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)

4.更复杂的继承关系

刚才我们的例子中, 只涉及到 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 关键字。

5.final关键字

final关键字总共有三种用法:
在这里插入图片描述

三.组合

组合和继承类似,也是一种表达类之间关系的方式,也是能够达到代码重用的效果,例如表示一个学校:

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

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字),仅仅是将一个类的实例作为另外一个类的字段。这是我们设计类的一种常用方式之一。

组合和继承的区别:

  • 组合表示 has - a 语义

在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师

  • 继承表示 is - a 语义

在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “” 一种动物

四.多态

1.向上转型

刚才例子中,我们写了这样一段代码:

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

这段代码也可以这样写:

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

此时 bird2 是一个父类 (Animal) 的引用,指向一个子类 (Bird) 的实例。这种写法称为 向上转型
在这里插入图片描述
向上转型发生的时机主要有三种:

  1. 直接赋值
  2. 方法传参
  3. 方法返回

直接赋值的方式我们已经演示了,接下来我们具体演示一下其他两种:

方法传参:

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

2.动态绑定

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

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


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); 
 	} 
} 

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 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() 实际调用了子类的方法

首先我们找到class文件所在目录,按住shift,右键点击Test,点击PowerShell,发现编译时调用的方法并不能确定真正调用的方法
在这里插入图片描述
因此, 在 Java 中,调用某个类的方法,究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象。 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定

3.方法重写

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

注意事项:

  1. 重写和重载完全不一样,不要混淆:
    在这里插入图片描述

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

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

  4. 重写的方法返回值类型不一定和父类的方法相同(可以是协变类型,返回值构成继承关系)

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

4.向下转型

向上转型是子类对象转成父类对象,向下转型就是父类对象转成子类对象。相比于向上转型来说,向下转型没那么常见,但是也有一定的用途
在这里插入图片描述
要想细究向下转型,我们来看一段代码:

 
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); 
 	} 
} 
 
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 + "正在飞"); 
 	} 
}

接下来我们在Test里让圆圆吃谷子:

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)

请注意,这里不是所有动物都是Bird!animal 本质上引用的是一个 Cat 对象,是不能转成 Bird 对象的, 运行时就会抛出异常。

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

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

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

5.super关键字

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

常见用法:

  1. super():调用父类的构造方法
  2. super.func():调用父类的普通方法
  3. super.data:调用父类的成员属性

superthis的区别:

  1. 代表的事物不同:
    super代表的是父类空间的引用
    this代表的是所属函数的调用者对象
  2. 使用前提不同:
    super必须要有继承关系才可以使用
    this不需要继承关系也可以使用
  3. 调用事物不同:
    super调用的是父类的构造方法
    this调用的是所属类的构造方法

在这里插入图片描述

6.构造方法中调用重写方法

这是一段有坑的代码,我们来看一看:
我们创建两个类, 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

这里调用了子类的方法,说明又发生了动态绑定:

  • 构造 D 对象的同时, 会调用 B 的构造方法
  • B 的构造方法中调用了 func 方法,此时会触发动态绑定, 会调用到 D 中的 func
  • 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态,值为 0

7.理解多态

了解了向上转型动态绑定方法重写之后,我们就可以使用 多态(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("♣"); 
 	} 
} 
//---------------------------------------

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 对应的实例相关), 这种行为就称为 多态

我们为什么要使用多态?有什么好处吗?

  1. 类调用者对类的使用成本进一步降低。
    封装是让类的调用者不需要知道类的实现细节。 多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可。
    因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低。
  2. 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
  3. 可扩展能力更强。如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低

猜你喜欢

转载自blog.csdn.net/weixin_55752048/article/details/122792304