⭐️前面的话⭐️
本篇文章带大家认识Java基础知识——组合,多态与接口,小伙伴们,好久不见,上篇博文《包与继承》留给读者的题选A。这篇文章将会详细介绍多态与接口,上次介绍的继承是阅读本文的基础,先来复盘一下继承吧,继承就相当于多张图纸实例一个对象,我们不妨将图纸分为两类,一类具有共性,另一类具有特性,则具有共性的图纸(类)称为父类(基类,超类),具有特性的图纸(类)称为子类,两者的关系是子类继承父类。然后,开始正文!
📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创,CSDN首发!
📆首发时间:🌴2021年11月23日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《Java核心技术》,📚《Java编程思想》,📚《Effective Java》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🙏作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
📌导航小助手📌
- 1.组合
- 2.多态
- 2.1向上转型
- 2.2运行时绑定
- 2.2.1概念
- 2.2.2重写方法
- 2.3编译时绑定
- 2.4向下转型
- 2.5多态
- 2.5.1理解多态
- 2.5.2多态的优点
- 3.抽象类
- 3.1abstract
- 3.2抽象类的特点
- 4.接口
- 4.1初见接口
- 4.2接口的特点
1.组合
在继承的关系中,子类(导出类)与父类(基类,超类)的关系是is - a
的关系,比如就鸟(子类)继承动物(父类),可以说鸟是一种动物。
而对于组合,就是在一个新类中使用一个或多个类,比如我们要创建一个学校类,我们都知道学校有老师,有学生,而且一般情况下肯定不止一个,所以学校类中会使用学生类和老师类,也就是说组合是hava
的关系,也就是学校里面有老师和学生。
class Student {
public String name;
public double score;
public int age;
public int classNo;
}
class Teacher {
public String name;
public int age;
public int classNo;
public String subject;
}
public class School {
public Student[] stu;
public Teacher[] tec;
}
2.多态
2.1向上转型
向上转型的前提是类之间存在继承关系,使用父类引用指向子类对象,这就叫做向上转型。
就比如Bird类继承了Animal类,然后使用父类Animal引用指向子类Bird对象,在Java中是允许父类引用指向子类对象的,过程中发生了向上转型,但是要注意向上转型后父类引用只能访问父类的成员变量和方法(子类没有重写父类方法的情况下),就栗子来说,Animal animal = new Bird("小鸟", 2);
这个animal引用只能访问eat方法,不能访问子类Bird的fly方法。
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
}
class Bird extends Animal{
public Bird(String name, int age) {
super(name, age);
}
public void fly() {
System.out.println(this.name + "正在飞翔!");
}
}
public class UpClass {
public static void main(String[] args) {
Animal animal = new Bird("小鸟", 2);
animal.eat();
}
}
⭐️常见发生向上转型有三种情况:
- 将子类对象的地址传给父类引用。
- 将子类对象的地址以参数形式传递给父类类型的形参。
- 在返回值为父类引用类型的方法中,将子类对象的地址作为返回值。
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
}
class Bird extends Animal{
public Bird(String name, int age) {
super(name, age);
}
public void fly() {
System.out.println(this.name + "正在飞翔!");
}
}
public class UpClass {
public static void func1(Animal an) {
an.eat();
}
public static Animal func2() {
return new Bird("小鸟", 2);
}
public static void main(String[] args) {
Animal animal1 = new Bird("小鸟", 2);
animal1.eat();
func1(new Bird("小鸟", 2));
Animal animal2 = func2();
animal2.eat();
}
}
2.2运行时绑定
2.2.1概念
运行时绑定也称动态绑定,它是在程序运行时所发生的一种行为。发生运行时绑定是基于向上转型的,所谓运行时绑定就是通过父类引用调用父类与子类同名的覆盖(重写/覆写)方法。为什么叫做运行时绑定呢?因为在程序运行的时候,方法的重写才发生,我们来看看一个含有重写代码的编译后的反汇编,会发现编译后调用的还是父类的方法。
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
}
class Bird extends Animal{
public Bird(String name, int age) {
super(name, age);
}
public void fly() {
System.out.println(this.name + "正在飞翔!");
}
@Override
public void eat() {
System.out.println(this.age + this.name + "正在慢慢地吃饭!");
}
}
class Cat extends Animal{
public Cat(String name, int age) {
super(name, age);
}
public void cute() {
System.out.println(name + "忙着装可爱!");
}
@Override
public void eat() {
System.out.println(this.age + this.name + "正在安静地吃饭!");
}
}
public class OverFunc {
public static void main(String[] args) {
Animal animal1 = new Bird("小鸟", 2);
animal1.eat();
Animal animal2 = new Cat("小猫", 1);
animal2.eat();
}
}
2.2.2重写方法
重写又称覆写,覆盖,顾名思义就是使用一个方法覆盖/覆写/重写了一个方法,但是不是任意两个方法都能发生重写,发生重写是有要求的,就像方法重载一样,都有着特定的规定,重写方法是在子类当中的,父类中的方法是被重写方法。
⭐️方法重写的条件:
- 方法名相同
- 参数列表相同(包括形参个数与形参类型)
- 返回值类型相同(协变返回类型除外)
协变返回类型指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更 “狭窄” 的类型。比如,子类与父类类型的返回值就有这样的关系。
⭐️方法重写注意事项:
- static修饰的方法不能重写。
- final修饰的方法不能重写。
- 权限为private的方法不能重写。
- 子类重写父类方法时,子类中重写的方法访问权限必须大于等于父类中被重写的方法。
知道了方法的重写我们来看看前面那个程序是否真如我们所说的,在运行时发生了重写。
结果是调用了子类的eat,说明重写是在程序运行时发生的,程序编译并没有发生重写。
2.3编译时绑定
刚刚介绍了运行时绑定,现在来说说编译时绑定,所谓的编译时绑定,就是程序发生了方法的重载,这个重载在编译时就会发生,不像重写是在运行时发生的。我们来看一段含有重载的代码的反汇编就明白了。
public class Func {
public static int add(int a, int b) {
return a + b;
}
public static int add(int a, int b, int c) {
return a + b + c;
}
public static int add(int a, int b, int c, int d) {
return a + b + c +d;
}
public static void main(String[] args) {
int x = add(1,2);
int y = add(1, 2, 3);
int z = add(1, 2, 3 ,4);
System.out.println(x);
System.out.println(y);
System.out.println(z);
}
}
⭐️方法重载的规则(复习):
- 方法名相同。
- 参数列表不同(包括形参类型与个数)。
- 返回值不做要求。
编译时绑定也称静态绑定,是程序编译时期的一种行为。
⭐️重写与重载的区别:
区别 | 重写 | 重载 |
---|---|---|
概念 | 方法名相同,参数列表相同,返回值相同(协变返回类型除外) | 方法名相同,参数列表不同,返回值不做要求 |
范围 | 继承关系(父子类关系) | 一个类 |
限制 | 子类的重写方法的权限要大于父类被重写的访问权限,访问权限不能是private,不能被final,static修饰 | 无权限要求,final,static修饰也可发生重载 |
2.4向下转型
向下转型用的很少且不安全。所谓的向下转型就是在向上转型的基础上,使用子类引用的值等于父类引用的值,这个过程需要强制转换。
还是看一个老栗子:
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void cute() {
System.out.println(name + "忙着装可爱!");
}
@Override
public void eat() {
System.out.println(this.age + this.name + "正在安静地吃饭!");
}
}
class Bird extends Animal {
public Bird(String name, int age) {
super(name, age);
}
public void fly() {
System.out.println(this.name + "正在飞翔!");
}
@Override
public void eat() {
System.out.println(this.age + this.name + "正在慢慢地吃饭!");
}
}
如果向下转型后类型匹配:
public class Instancesof {
public static void main(String[] args) {
Animal animal = new Cat("小猫", 1);
Cat cat = (Cat)animal;//向下转型
cat.eat();
}
}
向下转型后类型不匹配:
public class Instancesof {
public static void main(String[] args) {
Animal animal = new Cat("小猫", 1);
Bird Bird = (Bird) animal;
bird.eat();
}
}
为什么说它是不安全的呢,因为一个父类可能会有多个子类,向下转型可能会造成类型不匹配。所以为了避免这种异常的发生,我们在使用向下转型的时候,往往需要先判断类型是否匹配,这里我们可以使用关键字instanceof
对向下转型后子类引用是否类型匹配。
public class Instancesof {
public static void main(String[] args) {
Animal animal = new Cat("小猫", 1);
if (animal instanceof Bird) {
Bird bird = (Bird) animal;
bird.eat();
}
if (animal instanceof Cat) {
Cat cat = (Cat)animal;
cat.eat();
}
}
}
2.5多态
2.5.1理解多态
在理解向上转型情况下,我们就能来试着理解多态了,先不讲多态的概念,因为很抽象,先来一个例子。我们生活中存在许许多多的形状,比如圆,正方形,菱形,五角星,三角形等等,使用继承和重写的方法实现一个类Shape输出多种形状。
首先定义Shape类,在这个类中创建一个draw方法,然后创建其他的具体图形类继承Shape。
public class Shape {
public void draw() {
System.out.println("我要打印一个形状!");
}
}
class Circle extends Shape{
@Override
public void draw() {
System.out.println("○");
}
}
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("△");
}
}
class Rhombus extends Shape{
@Override
public void draw() {
System.out.println("◇");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("❀");
}
}
class Star extends Shape{
@Override
public void draw() {
System.out.println("☆");
}
}
class Square extends Shape{
@Override
public void draw() {
System.out.println("□");
}
}
方式1:使用父类数组存放子类的对象,然后循环调用子类的draw重写方法。
public class Test {
public static void main(String[] args) {
Flower flower = new Flower();
Square square = new Square();
Star star = new Star();
Rhombus rhombus = new Rhombus();
Circle circle = new Circle();
Triangle triangle = new Triangle();
Shape[] shapes = {flower, square, star, rhombus, circle, triangle};
for (Shape shape :shapes) {
shape.draw();
}
}
}
方式2:创建一个方法,形参是Shape类型,方法的具体内容就是调用draw方法。
public class Test2 {
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Flower flower = new Flower();
Square square = new Square();
Star star = new Star();
Rhombus rhombus = new Rhombus();
Circle circle = new Circle();
Triangle triangle = new Triangle();
drawMap(flower);
drawMap(square);
drawMap(star);
drawMap(rhombus);
drawMap(circle);
drawMap(triangle);
}
}
使用方式2输出各类的形状就叫做多态,当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态。
所谓多态就是一个引用能够表现出多种不同的形态,这就是多态,多态可以让你忘记对象的类型,就如这个栗子,你不需要关心shape引用的是哪一个子类,它会根据不同的子类对象而执行不同的方法或者说产生不同的行为。
2.5.2多态的优点
✨1.能够让类调用者对类的使用成本降低。
封装是让类的调用者不需要知道类的实现细节.
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
✨2.降低程序的“圈复杂度”,减少分支语句的使用。
如果没有多态,输出不同种类的形状时需要先判断到底调用哪一个类的draw方法,势必会使用很多的if-else分支语句,分支语句多了,“圈复杂度”也变得更复杂了.
圈复杂度是一种描述一段代码复杂程度的方式,一段代码如果平铺直叙, 那么就比较简单容易理解, 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,,这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构,一般情况下公司要求圈复杂不大于10.
3.抽象类
3.1abstract
在前面实现多态的时候父类Shape的draw方法,在运行时都没有被调用,因为都发生了重写,调用的是子类的draw方法,所以父类draw方法没有必要有具体实现。但是在Java中并没有C语言里面的声明,如果按C那样的形式声明方法,在Java中是会报错的。
于是Java中引入了抽象方法,抽象方法是一个没有具体实现的方法,它被关键字abstract
修饰,抽象方法的作用就是用来被重写。
包含抽象方法的类必须是抽象类,换一种说法,如果一个类包含一个或多个抽象方法,该类必须必须被限定为抽象类,也就是使用关键字abstract
修饰这个类。
在抽象类中可以定义成员变量,成员方法,就对类里面的属性和方法而言,与普通类不同的是抽象类里面含有抽象方法,其他都是一样的。除此之外,抽象类是普通类的进一步抽象,抽象类不能被实例化成对象,抽象类最大的作用就是为了被继承。
3.2抽象类的特点
- 包含抽象方法的类,称为抽象类。
- 抽象方法是一个没有具体实现的方法,类似与C语言的声明。
- 抽象类不能够被实例化成对象。
- 抽象类最大的作用就是为了被继承,抽象方法最大的作用就是被重写。
- 一个普通类继承了一个抽象类,则该普通类需重写抽象类中所有的抽象方法。
- 如果一个抽象类B继承了抽象类A,普通类C继承了抽象类B,则普通类C要重写抽象类A和抽象类B中所有的抽象方法。
- 一个抽象类B继承了另一个抽象类A,则抽象类B不需要重写抽象类A的方法。
- 抽象类中可以包含与普通类一样的成员方法和成员字段。
- 抽象类和抽象方法不能被final修饰。
abstract public class Shape {
abstract public void draw();
}
class Circle extends Shape{
@Override
public void draw() {
System.out.println("○");
}
}
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("△");
}
}
class Rhombus extends Shape{
@Override
public void draw() {
System.out.println("◇");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("❀");
}
}
class Star extends Shape{
@Override
public void draw() {
System.out.println("☆");
}
}
class Square extends Shape{
@Override
public void draw() {
System.out.println("□");
}
}
public class Test2 {
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Flower flower = new Flower();
Square square = new Square();
Star star = new Star();
Rhombus rhombus = new Rhombus();
Circle circle = new Circle();
Triangle triangle = new Triangle();
drawMap(flower);
drawMap(square);
drawMap(star);
drawMap(rhombus);
drawMap(circle);
drawMap(triangle);
}
}
4.接口
4.1初见接口
接口是抽象类的进一步抽象,想要创建一个接口,需要使用关键字替代interface
替代关键字class
,就像类一样,可以在interface
前面使用public
修饰(需要与文件名相同),如果不添加关键字public
,就是默认的包访问权限,接口和类一样不能使用protected
private
修饰。
接口名一般以大写I开头,接口中的普通方法,不能有具体实现,如果非要实现需要使用default
修饰。
public interface Shape {
public void draw();
default public void print() {
System.out.println("我非要实现接口中的方法!");//default后面的public可以不加
}
}
4.2接口的特点
- 接口是抽象类的进一步抽象,使用关键字 i n t e r f a c e interface interface定义接口,接口只能被public修饰或者不被访问权限关键字修饰。
- 接口不能使用new来实例化对象,因为它是极度抽象的。
- 接口中的普通方法不能被具体实现,非要实现请使用 d e f a u l t default default修饰。
- 接口中可以有静态方法。
- 接口里面所有的抽象方法默认都是public abstract修饰的,接口中所有方法都是public修饰的。
- 类可以通过关键字 i m p l e m e n t s implements implements来实现接口。
- 一个类最多只能继承一个类或抽象类,同时可以实现多个接口,类继承写在前面,接口实现写在后面,使用逗号隔开。
- 当一个普通类实现了一个接口,必须重写这个接口及其扩展接口的所有方的抽象法。
- 接口内的成员变量都是 p u b l i c s t a t i c f i n a l public\ static\ final public static final修饰的。
- 接口与接口之间存在扩展的关系,使用关键字 e x t e n d s extends extends表示扩展关系。
- 当一个类实现接口的时候,重写的方法访问权限必须是 p u b l i c public public。
使用接口实现图形输出:
public interface Shape {
public void draw();
default public void print() {
System.out.println("我非要实现接口中的方法!");//default后面的public可以不加
}
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("○");
}
}
class Triangle implements Shape {
@Override
public void draw() {
System.out.println("△");
}
}
class Rhombus implements Shape {
@Override
public void draw() {
System.out.println("◇");
}
}
class Flower implements Shape {
@Override
public void draw() {
System.out.println("❀");
}
}
class Star implements Shape {
@Override
public void draw() {
System.out.println("☆");
}
}
class Square implements Shape {
@Override
public void draw() {
System.out.println("□");
}
}
public class Test {
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Flower flower = new Flower();
Square square = new Square();
Star star = new Star();
Rhombus rhombus = new Rhombus();
Circle circle = new Circle();
Triangle triangle = new Triangle();
drawMap(flower);
drawMap(square);
drawMap(star);
drawMap(rhombus);
drawMap(circle);
drawMap(triangle);
}
}
使用接口描述动物行为:
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(this.name + "正在吃饭");
}
}
interface IRun{
void running();
}
interface ISwim{
void swimming();
}
interface IFly{
void flying();
}
interface ISkip{
void skipping();
}
class Frog extends Animal implements ISkip, ISwim{
public Frog(String name) {
super(name);
}
@Override
public void swimming() {
System.out.println(name + "擅长蛙泳!");
}
@Override
public void skipping() {
System.out.println(name + "要跳起来!");
}
}
class Bird extends Animal implements IFly{
public Bird(String name) {
super(name);
}
@Override
public void flying() {
System.out.println(name + "正在自由飞翔!");
}
}
class Cat extends Animal implements IRun,ISkip{
public Cat(String name) {
super(name);
}
@Override
public void running() {
System.out.println(name + "的狂奔!");
}
@Override
public void skipping() {
System.out.println(name + "也会跳!");
}
}
class Duck extends Animal implements ISwim, IRun{
public Duck(String name) {
super(name);
}
@Override
public void running() {
System.out.println(name + "也会努力奔跑的!");
}
@Override
public void swimming() {
System.out.println(name + "也会游泳!");
}
}
public class TestDemo {
public static void running(IRun iRun) {
iRun.running();
}
public static void swimming(ISwim iSwim) {
iSwim.swimming();
}
public static void skipping(ISkip iSkip) {
iSkip.skipping();
}
public static void flying(IFly iFly) {
iFly.flying();
}
public static void main(String[] args) {
Bird bird = new Bird("小鸟");
Cat cat = new Cat("小猫咪");
Frog frog = new Frog("小青蛙");
Duck duck = new Duck("小鸭子");
flying(bird);
running(cat);
running(duck);
skipping(frog);
skipping(cat);
swimming(frog);
swimming(duck);
}
}