【JAVA SE】三万字终极魔典 面向对象编程深度讲解(包+继承+多态+抽象类+接口 全面剖析)

温馨提示

大家好我是Cbiltps,在我的博客中如果有难以理解的句意难以用文字表达的重点,我会有配图。所以我的博客配图非常重要!!!

而且很多知识在代码的注释里,所以代码注释也非常重要!!!

这篇文章前后逻辑顺序非常重要,一定要从前往后看,慢慢看!!!

此文章转自自己的CSDN!

开篇介绍( 知识点比较多 务必耐心看完!)

等后面我会写关于类和对象的博客,其实在学习本篇博客之前一定要类和对象有一定的了解!就是大家先学习类和对象的博客,然后再看这篇面向对象文章,你就会有一个阶梯式的理解

紧接着,在这篇文章的尾部,我会展开一个简单且知识点全面的一个关于面向对象的训练(最后面直接贴链接),大家拭目以待吧!学习完后你就会对面向对象有一个初步的认识

还有一点:这篇文章横向拓展比较多,全程三万多字!大家在学习的时候会看到丰富的知识点,覆盖面也比较全!

那么,今天这篇博客的知识点在小米阿里巴巴百度VIVO腾讯携程贝壳美团头条网易京东滴滴等公司常考!!!

本章重点

  • 继承
  • 多态
  • 抽象类
  • 接口

正文开始


1. 包


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

上面的话晦涩难懂,我们先不要纠结这个,直接举例子演示(看下面)!

1.1 导入包中的类

比如说打印数组的时候就导入了包中的类:

扫描二维码关注公众号,回复: 13684219 查看本文章
package com.company;

import java.util.Arrays;//调用了util这个包中的Arrays这个类

public class Main {

    public static void main(String[] args) {
	    int[] array = {1,2,3,4,5};
        System.out.println(Arrays.toString(array));//用字符串的方式打印数组
    }
}
复制代码

注意一个问题: toString这个方法是由类名(Arrays)调用的,所以这个方法就是一个静态方法按住CTRL后鼠标点进去,进入Arrays.java文件,如下图)! 在这里插入图片描述 所以所有通过类名直接调用的方法就一定是静态方法,我们不用管这个方法是怎么执行的,这个是官方写好的,直接调用即可!

那么,它在哪个包底下呢?

在上面打开文件(Arrars.java)的最前面就可以看到的,如下 在这里插入图片描述 我们可以找到这个文件(Arrars.java)的路径: 在这里插入图片描述 关于上面的package关键字是什么,一会说!而且再看下面一点点(Arrars.java中)还有import关键字在这里插入图片描述 然后提出疑问:importpackage有什么区别?

  • package 是你运行程序后生成的.class文件全部放在了你所定义的包中,便于以后调用管理。
  • import 则是在编写程序的时候需要调用某个包中的类。

也就是说,如果要用到一些Java类库里面代码的时候,我们都需要import来导入的!

然后再举一个导入例子:

//导入方式1
package com.company;

import java.util.Date;//在这里导入

public class Main {

    public static void main(String[] args) {
        Date date = new Date();
    }
}
复制代码
//导入方式2
package com.company;

public class Main {

    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();//这导入也可以,但是比较麻烦,推荐方式1
    }
}
复制代码

记住:只能导入一个具体的类,不能导入一个具体的包在这里插入图片描述

然后紧接着下一个问题:我们看到有人是这样导入包的,这里的*是什么意思?

import java.util.*;
复制代码
  • * 是一个通配符:意思是导入这个包底下所有的类

疑问:util下面有很多的类,难道一下子全部导入了吗?

不是的,Java处理的时候,需要谁,它才会拿谁

C语言里面,通过include关键字导入之后,会把头文件里面的内容全部拿过来

但是,这样子(import java.util.*;)导入的范围太广了,你有可能把握不住,有时候会有冲突(如下),建议导入具体的类名在这里插入图片描述 但是,假设你想用另一个包底下的Date,就识别不了了(看下图): 在这里插入图片描述 解决方法就是:使用完整的类名导入在这里插入图片描述

//代码组织如下:
package com.company;

import java.util.*;
import java.sql.*;
//以上两个包中都有Date类,为了避免冲突,使用如下操作

public class Main {

    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();//使用完整的类名导入
    }
}
复制代码

1.2 静态导入

一般情况下,静态导入用的非常的少,了解即可!

package com.company;

import static java.lang.System.*;
import static java.lang.Math.*;

public class Main {

    public static void main(String[] args) {
        out.println("123");//静态导入写起来方便
        out.println(max(12, 23));//但是,不提倡这样写,不方便阅读,稀奇古怪
    }
}
复制代码

1.3 将类放到包中

废话不多说,直接上步骤

  1. 新建一个包

在这里插入图片描述 2. 创建类 在这里插入图片描述

同时可以看到磁盘上的目录结构已经被 IDEA 自动创建出来了 在这里插入图片描述

基本规则(注意以下几点):

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

经过上面的步骤后,返回第一节的第一句话 (package) 是组织类的一种方式,使用包的主要目的是保证类的唯一性

如何保证类的唯一性呢? 其实就是磁盘上的同一目录下(同一个包下)只能有一个同名的文件(类),并且包名不一样了,路径也不一样,也就互相不干扰了!

1.4 包的访问权限控制

包访问权限:顾名思义,就是只能在当前包中使用

举个例子:当你的成员变量不加任何的访问修饰限定词的时候,就是包访问权限

由于在不同的包中展示,不方便直接代码展示,所以就截图展示了(比较乱,见谅!) 在这里插入图片描述 由此可见,cbiltps包无法访问company包中的val值,所以包访问权限只能在当前包中使用

1.5 常见的系统包

  1. java.lang:系统常用基础类(String、Object)

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


2. 继承


在学习本小节之前先来复习一下知识点:面向对象的基本特征(共四点 先说两点)

  • 封装:不必要公开的数据成员和方法,使用private关键字修饰(为了安全性!
  • 继承:对共性的抽取,使用extends进行处理(代码可以重复使用!

2.1 了解继承

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

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

例如, 设计一个类表示动物(直接上代码):

class Animal {
    //动物的名字和年龄都是共有的!
    public String name;
    public int age;

    //包括吃也是共有的行为!
    public void eat() {
        System.out.println(name + "eat()");
    }
}

class Dog extends Animal {
    //抽取完后大大减少了代码量!
}

class Bird extends Animal {
    public String wing;

    public void fly() {
        System.out.println(name+"fly()" + age);
    }
}
复制代码

此时,Animal 这样被继承的类, 我们称为父类基类超类,对于像 DogBird 这样的类,我们称为子类派生类

从逻辑上讲,DogBird 都是一种 Animal (is - a 语义);

和现实中的儿子继承父亲的财产类似,子类也会继承父类的字段和方法,以达到代码重用的效果。 在这里插入图片描述 extends 英文原意指 "扩展",而我们所写的类的继承,也可以理解成基于父类进行代码上的 "扩展";

例如我们写的 Bird 类, 就是在 Animal 的基础上扩展出了 fly 方法。

2.2 语法规则(比较枯燥)

基本语法:

class 子类 extends 父类 {
}
复制代码

注意

  • 使用 extends 指定父类
  • Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承)

在这里插入图片描述

  • 子类会继承父类的所有 public 的字段和方法
  • 对于父类的 private 的字段和方法,子类中是无法访问

在这里插入图片描述

  • 子类的实例中,也包含着父类的实例,可以使用 super 关键字得到父类实例的引用

上面简单的总结后看另外一个问题: 在这里插入图片描述 得出结论:子类和父类字段同名的情况下优先访问的是子类的 !

而它的内存图是这样子的: 在这里插入图片描述

2.3 子类的构造方法

看一个问题:根据上面的表示动物类的代码中添加父类的构造方法后为什么会产生下面的错误? 在这里插入图片描述 根据上面的报错,我们得到下面的结论:

  • 父类构造方法不能被子类继承
  • 子类的构造方法中必须要调用父类的构造方法

而且注意:调用父类构造方法的时候

  • 子类通过 super(参数列表)调用父类构造方法,调用super的语句必须放在子类构造方法的第一行
  • 若子类构造方法中没有显示调用父类构造方法,则系统默认调用父类无参的构造方法;若父类中没有定义无参数的构造方法,编译出错

所以,按照下面的样子改即可: 在这里插入图片描述 上面的知识点明白之后,我们补充完主类后,画一下内存图加强理解):

//主类的代码如下:
public class TestDemo {
    public static void main(String[] args) {
        Dog dog = new Dog("哈士奇",19);
        System.out.println(dog.name);
        dog.eat();

        Bird bird = new Bird("喜鹊",18,"我要的飞翔");
        System.out.println(bird.name);
        bird.eat();
        bird.fly();
    }
}
复制代码

在这里插入图片描述

2.4 super 和 this 的区别

在之前的学习中已经遇见 superthis 两个关键字,博主根据自身所学及博客参考做出如下总结:

super: 可以理解为父类对象的引用(是依赖对象的),不能出现在静态环境(包括:static变量,static方法,static语句块)中因为 static 不依赖对象)!

  • super(); //调用父类的构造方法
  • super.func(); //调用父类的普通方法
  • super.data; //调用父类的成员属性

this: 可以理解为指向本对象的指针,它代表当前对象名(在程序中易产生二义性之处,应使用 this指明当前对象;如果函数的形参类中的成员数据同名,这时需用 this指明成员变量名)!

  • this(); //调用本类中另一种形成的构造方法

注意点与区别总结:

  • super();this();区别是:super();从子类中调用父类的构造方法,this();在同一类内调用其它方法
  • super();this();均需放在构造方法内第一行
  • 有时候 thissuper 不能同时出现在一个构造函数里面,因为 this 必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过
  • this();super();都指的是对象,所以,均不可以在 static环境中使用(包括:static变量,static方法,static语句块)

在这里插入图片描述

2.5 访问权限

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

  • private: 类内部能访问, 类外部不能访问
  • 默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问
  • protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问
  • public : 类内部和类的调用者都能访问

重要的是下面的范围图: 在这里插入图片描述

private关键字(补充的一点):

这里有一个问题

父类的 private 修饰的成员变量是否被继承了?

这个问题的答案有点模糊,有的书上说是继承了的;

但有的书上说不是,建议我们采用没有没继承的答案!

原因看下面:

//父类
class A {
    private int a;//使用private修饰
}
复制代码
//子类 这段代码是错
class B extends A { //继承
    public void func() {
        System.out.println(this.a);//这里无法访问就说是没有没继承的!
    }
}
复制代码

这个知识点先就写到这里,后期有需要的话经过百度之后有可能会补充!

大家也可以在评论区自由发挥!

protected关键字:

刚才我们发现,如果把字段设为 private,子类不能访问;

但是设成 public,又违背了我们 "封装" 的初衷;

两全其美的办法就是 protected 关键字。

  • 对于类的调用者来说,protected 修饰的字段和方法是不能访问
  • 对于类的 子类同一个包的其他类 来说,protected 修饰的字段和方法是可以访问

什么时候下用哪一种呢?

我们希望类要尽量做到 "封装",即隐藏内部实现细节,只暴露出必要的信息给类的调用者。

因此我们在使用的时候应该尽可能的使用比较严格的访问权限。

例如如果一个方法能用 private,就尽量不要用 public

另外,还有一种简单粗暴的做法:将所有的字段设为 private, 将所有的方法设为public

不过这种方式属于是对访问权限的滥用,还是更希望同学们能写代码的时候认真思考,该类提供的字段方法到底给 "谁" 使用(是类内部自己用,还是类的调用者使用,还是子类使用)。

2.6 更复杂的继承关系

这里有个不成文的规则:继承层次最好不要超过三个!

时刻牢记,我们写的类是现实事物的抽象。而我们真正在公司中所遇到的项目往往业务比较复杂,可能会涉及到一 系列复杂的概念,都需要我们使用代码来表示,所以我们真实项目中所写的类也会有很多。类之间的关系也会更加 复杂。

但是即使如此,我们并不希望类之间的继承层次太复杂。一般我们不希望出现超过三层的继承关系。如果继承层 次太多,就需要考虑对代码进行重构了。

如果想从语法上进行限制继承,就可以使用 final 关键字 !

2.7 final 关键字

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

final int a = 10;
a = 20; //编译出错
复制代码

final 关键字也能修饰(叫做密封类),此时表示被修饰的类就不能被继承 !

final 关键字的功能是限制类被继承,"限制" 这件事情意味着 "不灵活"。

在编程中,灵活往往不见得是一件好事,灵活可能意味着更容易出错

而我们常见的String类就是final修饰的: 在这里插入图片描述


3. 多态


从字面上理解,就是一种事物多种形态。

但是,面试官问的时候不能这样回答!

了解多态需要一个过程!

3.1 向上转型

根据上面的继承关系,我们进行探讨:

public static void main(String[] args) {
        /*Dog dog = new Dog("旺财",20);
        Animal animal = dog;*/
        
        //把上面的代码简化一下
        Animal animal = new Dog("旺财",23);//向上转型   
        //其实就是:父类引用 引用 子类对象
    }
复制代码

知识点补充:父类引用访问成员

学到这里我在添加一个知识点(这一个知识点作为 继承访问的补充),

然后我们再次重新展示一下代码(为了方便我直接把知识点写写进了代码里,请大家注意查收):

//Animal类
class Animal {
    public String name = "动物";
    public int age;
    protected int count;

    public Animal(String name,int age) {
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(name+" eat()");
    }
}
复制代码
//Bird类 继承于 Animal
lass Bird extends Animal {
    public String wing;
    public String name = "鸟类";

    public Bird(String name, int age, String wing) {
        super(name, age);
        this.wing = wing;
    }

    public void fly() {
        System.out.println(super.name + "fly()");
    }
}
复制代码
//主类的主方法
public static void main(String[] args) {

        Animal animal2 = new Bird("wuya",12,"wuya fly!");//这里发生向上转型
        animal2.eat();//可以调用eat方法

        System.out.println(animal2.name);//这里其实访问的是父类的name

//        注意了:重点知识在这里!!!
//        System.out.println(animal2.wing); 无法访问的
//        因为animal的类型是Animal类型
//        意思就是:通过父类引用,只能访问父类自己的成员!

    }
复制代码

什么情况下会发生向上转型?

  1. 直接赋值(就是上面的情况)
public static void main(String[] args) {
        /*Dog dog = new Dog("旺财",20);
        Animal animal = dog;*/
        
        //把上面的代码简化一下
        Animal animal = new Dog("旺财",23);//向上转型   
        //其实就是:父类引用 引用 子类对象
    }
复制代码
  1. 作为函数的参数
public class TestDemo2 {

    public static void func(Animal animals) {

    }

    public static void main(String[] args) {

        Dog dog = new Dog("金毛",20);
        func(dog);//在这里发生向上转型
    }
}
复制代码
  1. 作为方法的返回值
 public static Animal fun2(Animal animalss) {
        Dog dog = new Dog("huahua",23);
        return dog;//在这里发生向上转型
    }
复制代码

3.2 动态绑定

Java中有两种多态运行时多态(动态绑定)编译时多态(静态绑定)

编译时多态就是重载来实现的,根据你给的参数以及个数的不同,来推导出你调用那个函数!

运行时多态是怎样的呢?往下看...

首先一个问题: 在这里插入图片描述 因为此时发生了动态绑定!

发生动态绑定两个条件:

  • 父类引用 引用 子类对象
  • 通过这个父类引用,调用父类和子类同名的覆盖方法

大家注意了:动态绑定多态的基础 !!!

这个时候是不是还是有点不明白这个动态是啥意思,接着往下看...

那就来看看下面的字节码文件:

在此之前,我们来看看Java中如何打开反汇编代码? 在这里插入图片描述 我们来看一下这个反汇编代码(main 方法): 在这里插入图片描述 我们看到反汇编代码中调用的是 Animal.eat; (父类的 eat 方法),但是运行的时候调用的为啥是 dog.eat(); (引用的子类对象的 eat 方法)? 在这里插入图片描述 在这里,在编译的时候不能够确定此时调用谁的方法;在运行的时候才知道调用谁的方法

这个叫运行时绑定,也叫动态绑定!

知识点补充1:在构造方法中调用重写的方法

直接看代码:

//Animal类
class Animal {
    public String name = "动物";
    public int age;

    public Animal(String name,int age) {
        eat();//在父类中调用父类和子类重写的eat方法,也会发生所谓的动态绑定!
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(name+" eat()");
    }
}
复制代码
//Dog类 继承于 Animal类
class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);//显示调用构造方法
    }

    @Override //这个是注解
    public void eat() {
        System.out.println(name+"crazy eat()");
    }
}
复制代码
//主类的主方法
public static void main(String[] args) {
        Dog dog = new Dog("wawa",23);//这里创建对象,直接运行,看下面的结果
    }
复制代码

运行结果如下: 在这里插入图片描述 说明:在父类中调用父类和子类重写的eat方法也会发生所谓的动态绑定

知识点补充2:静态绑定

class Dog {
    public void func(int a) {
        System.out.println("int");
    }

    public void func(int a,int b) {
        System.out.println("int,int");
    }

    public void func(int a,int b,int c) {
        System.out.println("int,int,int");
    }
}
复制代码
//主类的主方法:
public static void main(String[] args) {
        Dog dog = new Dog("haha",19);
        dog.func(10);
    }
复制代码

在这里我们打开 PowerShell 窗口查看反汇编代码: 在这里插入图片描述 此时这里发生的就是:静态绑定

就是根据你给的参数的类型个数推导出你调用的那个函数

3.3 方法重写

而这个所谓的父类和子类同名的覆盖方法就是覆写/重写/覆盖(Override)!

此时的重写满足下面的条件

  1. 方法名相同
  2. 参数列表相同个数类型
  3. 返回值相同
  4. 必须是父子类的情况下

而且注意:

  • 静态的方法不能重写
  • 子类的访问修饰限定符要大于等于父类的访问修饰限定
  • private 修饰的方法不能重写
  • final 修饰的方法不能重写

但是有一个 special time(很少有书上写,考试也很少出现):

重写的时候,返回值可以不一样

但是要满足下面的情况: 在这里插入图片描述 如果你遇见选择题的时候,选择最正确的一个就可以辣!

这里有一点要讲一下

上面的 Animal类 返回的是 AnimalDog类 返回的是 Dog

它们的返回值构成了一种类型,叫协变类型

如果你的返回值发生了协变类型,我们也说发生了重写

3.4 重写和重载的区别(重新整理)

重写(Override): 子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名参数列表返回类型(除子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或覆盖即外壳不变,核心重写!

  • 发生方法重写的两个方法返回值(除了上面写到的special time和被重写方法返回值类型的子类)方法名参数列表必须完全一致(子类重写父类的方法)
  • 子类方法的访问级别不能低于父类相应方法的访问级别
  • 覆盖的方法所抛出的异常和被覆盖方法的所抛出的异常一致或者是其子类(子类异常不能大于父类异常)
  • privatefinalstatic 修饰的方法不能重写

重载(Overload): 在一个类中,同名的方法如果有不同的参数列表参数类型不同参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,所以不能通过返回类型是否相同来判断重载

  • 方法名相同,参数列表不同(参数顺序、个数、类型)
  • 方法返回值、访问修饰符任意

注意点与区别总结:

  • 重写实现的是运行时的多态,而重载实现的是编译时的多态
  • 重写的方法参数列表必须相同(一般情况下);而重载的方法参数列表必须不同
  • 重写的方法的返回值类型只能是父类类型或者父类类型的子类,而重载的方法对返回值类型没有要求

在这里插入图片描述

3.5 向下转型

直接看下面代码(知识点全部写进了注释里):

public static void main(String[] args) {
        Animal animal3 = new Bird("lala",12,"flyyyyy");
        Bird bird = (Bird)animal3;//强行转换
        bird.fly();//这里可以调用fly方法

        //在这里不建议这样写,有的时候是错的(不是非常的安全)!
        //因为不是所有的动物都是鸟,逻辑上就是颠覆认知的!

        //你可以向下转型;
        //前提是:这个引用(animal3) 引用了 你将要向下转型的这个对象(bird)!
    }
复制代码

那为什么说不是非常安全的呢?

//代码这样子写是错的!
public static void main(String[] args) {
        Animal animal4 = new Dog("aa",23);
        Bird bird = (Bird)animal4;//这里就会报类型转换异常 
        //因为:不是所有的动物都是鸟!
        bird.fly();
    }
复制代码

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

public static void main(String[] args) {
        Animal animal4 = new Dog("aa",23);
        
        if (animal4 instanceof Bird) { //这里if语句没进来
            Bird bird = (Bird)animal4;
            bird.fly();
        }
        //运行之后就是什么都没有
    }
复制代码

instanceof 可以判定一个引用是否是某个类的实例

如果是,则返回 true!

这时再进行向下转型就比较安全了。

3.6 理解多态

我们先来写一段代码:

class Shape {
    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 TestDemo1 {

    public static void drawMap(Shape shape) {
        shape.draw();//动态绑定(运行时),这里调用了重写的draw方法
    }

    public static void main(String[] args) {
        //它的类以及下面对象的类都是Shape类的子类,目的就是为了发生向上转型!
        Rect rect = new Rect();
        drawMap(rect);
        Flower flower = new Flower();
        drawMap(flower);
    }
}
复制代码

简单的定义一下:

通过一个引用调用一个 draw方法(父类和子类覆盖的方法),会有不同的表现形式(取决于引用谁的对象),这就是多态

多态的大前提就是一定要向上转型,且调用一个父类方法(子类覆盖,调用父类)!

使用多态的好处是什么?

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

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

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

这也贴合了《代码大全》中关于 "管理代码复杂程度" 的初衷!

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

看代码:

//不基于多态
public static void main3(String[] args) {
        Rect rect = new Rect();
        Flower flower = new Flower();
        Triangle triangle = new Triangle();

        String[] shapes = {"triangle", "rect", "triangle", "rect", "flower"};
        for (String s : shapes) {
            if(s.equals("triangle")) {
                triangle.draw();
            }else if(s.equals("rect")) {
                rect.draw();
            }else {
                flower.draw();
            }
        }
    }
复制代码
//基于多态:
//明显感觉这样子的代码更更高级,量更少!
public static void main4(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();
        }
    }
复制代码

什么叫 "圈复杂度" ?

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

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

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

3:可扩展能力更强

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

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

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

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

4. 抽象类

4.1 了解抽象类

文章写到这里,其实是有一点瑕疵的,不知道大家有没有发现!狗头保命!!!

我们回到打印图形的代码(父类):

class Shape {
    public void draw() {
        System.out.println("打印图形中...");
    }
}
复制代码

我们来思考一下,

其实System.out.println("打印图形中...");这一段代码是没有意义的,因为后面就是类的继承和方法的重写(没有实际工作)!

那接下来的代码是不是可以这样写呢?

//其实这样写是错误的
class Shape {
    public void draw();
}
复制代码

然后就有了下面的写法:

//这里加上 abstract 关键字表示这是一个抽象类
abstract class Shape {
    //而这里表示的是一个抽象方法
    abstract public void draw();//抽象方法没有方法体(没有 { } , 不能执行具体代码)!
    
    //同时也注意:包含抽象方法的类叫抽象类
}
复制代码

4.2 语法规则

  1. 抽象类不能直接实例化(会直接报错)

在这里插入图片描述 2. 抽象类中可以有普通的方法和成员 3. 普通类继承了抽象类,这个普通类中必须重写抽象类的所有抽象方法(可以被重写和调用) 4. 抽象方法不能是 private 修饰的

不仅如此,还有一些特殊的规定!

  1. 一个抽象类B继承了抽象类A,那么这个抽象类A中可以不实现抽象类A的抽象方法!
  2. 在上条继承关系的基础上,普通类C继承了抽象类B,那么A和B中的抽象方法必须被重写!
  3. 抽象类和抽象方法是不能被 final 修饰!

看代码是这样子的:

abstract class A {
    abstract public void draw();//抽象方法
}
复制代码
abstract class B extends A{ //这里继承于Shape
    public abstract void funcA();//这里也是抽象方法
    //注意:一个抽象类B继承了抽象类A,那么这个抽象类A中可以不实现抽象类A的抽象方法!
}
复制代码
class C extends B {
//在上面继承关系的基础上,普通类C继承了抽象类B,那么A和B中的抽象方法必须被重写!
    @Override
    public void funcA() {
    }

    @Override
    public void draw() {
    }
}
复制代码

4.3 抽象类的作用

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

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

使用抽象类的场景就如上面的代码,实际工作不应该由父类完成,而应由子类完成

那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的!

这里其实有一个提示报错的功能,父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题!

5. 接口

这一节的知识点非常繁琐(语法比较多),任何知识点都在代码注释里(方便直接理解)!

知识点都在代码注释里!

知识点都在代码注释里!

知识点都在代码注释里!

//这个是抽象类借此引出下面的接口!
abstract class Shape {
    abstract public void draw();
}
复制代码

接口是抽象类的更进一步!

抽象类中还可以包含非抽象方法和字段

而接口中包含的方法都是抽象方法,字段只能包含静态常量

5.1 了解接口及简单语法规则

//使用 interface 定义一个接口
interface IShape {
    //abstract public void draw();//这里不加abstract public也是可以的!
    //而且注意:接口里面的所有的方法都是 pubilc 的!
    void draw();//抽象方法

    //接口中的普通方法不能有具体的实现!
    //public void func() { //这个方法是错误的!
    //} //error

    //如果要实现,就要使用default关键字修饰这个方法!
    default public void func() {
        System.out.println("接口中的普通方法...");
    }

    //接口中可以有静态方法!
    public static void funcStatic() {
        System.out.println("接口中的静态方法...");
    }
}
复制代码

接口小总结:

  • 接口中的方法一定是抽象方法,因此可以省略 abstract
  • 接口中的方法一定是 public,因此可以省略 public
//类和接口之间的关系是通过 implements关键字 实现的!
class Rect implements IShape {
    //当一个类实现了一个接口,就必须重写接口中的抽象方法! 
    @Override
    public void draw() {
        System.out.println("♦");
    }
    
    @Override
    public void func() {
        System.out.println("重写接口当中的默认方法");
    }
}

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

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

class Cycle implements IShape {
    @Override
    public void draw() {
        System.out.println("●");
    }
}
复制代码
//测试主类:
public class TestDemo3 {

    public static void drawMap(IShape iShape) {
        iShape.draw();//动态绑定(运行时),这里调用了重写的draw方法
    }

    public static void main(String[] args) {

        //IShape iShape = new IShape();//错误的,接口不能实例化!
        IShape iShape = new Rect();//但是可以发生向上转型!
        iShape.draw();

        //它的类以及下面对象的类都是Shape类的子类,目的就是为了发生向上转型!
        Rect rect = new Rect();
        Flower flower = new Flower();
        drawMap(rect);
        drawMap(flower);
    }
}
复制代码

小总结:

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

扩展(extends) 和 实现(implements)区分:

  1. 扩展指的是当前已经有一定的功能了,进一步扩充功能
  2. 实现指的是当前啥都没有,需要从头构造出来

5.2 实现多个接口及其他语法规则

同样的,直接上代码:

//定义接口 IA
interface IA {
    //接口中的成员变量,默认是 public static final 的!
    //public static final int a = 10;//可以写成下面的样子!
    int A = 10;//相当于是常量

    void funcA();//方法就这样写,默认是 pubilc abstract 的!
}
复制代码
//定义接口 IB
interface IB { 
    void funcB();
}
复制代码
//定义抽象类 B
abstract class B {
    //抽象类B
}
复制代码

有的时候我们需要让一个类同时继承自多个父类

这件事情在有些编程语言通过 多继承 的方式来实现的,

然而 Java 中只支持单继承,一个类只能 extends 一个父类!

但是可以同时实现多个接口,也能达到多继承类似的效果!

//下面的类A继承了抽象类B(普通类也可以),但是只能单继承!
//同时,也可以实现多接口,接口之间用逗号隔开!
class A extends B implements IA,IB {
    @Override
    public void funcA() { //当一个类实现一个接口并在重写方法的时候,方法必须是 pubilc 的!
                          //如果不是 pubilc 权限更加严格了,所以无法覆写
        System.out.println("Override funcA");
        System.out.println(A);//当然也可以访问接口中的东西!
    }

    @Override
    public void funcB() {
        System.out.println("Override funcB");
    }
}
复制代码

这样设计有什么好处呢?

时刻牢记多态的好处,让程序猿忘记类型

有了接口之后,类的使用者就不必关注具体类型,

而只关注某个类是否具备某种能力

补充提示:

  1. 我们创建接口的时候,接口的命名一般以大写字母 I 开头
  2. 接口的命名一般使用 "形容词" 词性的单词(接口表达的含义是 具有 xxx 特性
  3. 阿里编码规范中约定,接口中的方法和属性不要加任何修饰符号保持代码的简洁性

5.3 接口之间的继承

刚刚说了,类和接口之间的关系是 implements 操作的。

我想提出的问题是:

那么接口接口之间会存在什么样的关系呢?

interface IA1 {
    void funcA();
}

//接口和接口之间可以使用extends来操作他们的关系,此时,这里面意为:拓展。
interface IB1 extends IA1 {
    void funcB();
}

class C implements IB1 {
    @Override
    public void funcB() {
        System.out.println("光重写B还不够!");
    }

    @Override
    public void funcA() {
        System.out.println("还要重写A!");
    }
}
复制代码

一个接口IB1通过extends拓展另一个接口IA1的功能

此时当一个类C通过implements实现这个接口IB1的时候,

此时重写的方法不仅仅是IB1的抽象方法,还有他从IA1接口拓展来的功能(方法)

5.4 接口使用实例

在这一个小结给大家介绍三个常用的接口

Comparable接口

直接上代码:

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 +
                '}';
    }
}

public class TestDemo7 {

    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(12,"huahua",56);
        students[1] = new Student(23,"wewe",34);
        students[2] = new Student(32,"rous",78);

        System.out.println(Arrays.toString(students));
        Arrays.sort(students);//问题就出在sort这个方法里
        System.out.println(Arrays.toString(students));
    }

}
复制代码

按照上面运行会报错的,因为没有一个可以排序的依靠(没有一个东西作比较)!

大家感兴趣可以多看看底层的代码! 在这里插入图片描述 把代码修改一下后是这样子的:

//这里直接实现一个Comparable接口
class Student implements Comparable<Student> {
    public int age;
    public String name;
    public double score;

    //这里要重写下面的方法,就是依靠什么条件来排序的,下面是依靠年龄举例!
    @Override
    public int compareTo(Student o) { //谁调用这个方法 谁就是this
        /*if(this.age > o.age) {
            return 1;
        }else if(this.age == o.age) {
            return 0;
        }else {
            return -1;
        }*/

        //更简单的实现(从小到大)
        return this.age - o.age;
    }

    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 +
                '}';
    }
}

public class TestDemo7 {

    //了解compareTo是如何用的!
   /* public static void main(String[] args) {
        Student student1 = new Student(98,"huahua",56);
        Student student2 = new Student(45,"wewe",34);

        System.out.println(student1.compareTo(student2));//这里是一个大于零的数字
    }*/

    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(12,"huahua",56);
        students[1] = new Student(45,"wewe",34);
        students[2] = new Student(32,"rous",78);

        System.out.println(Arrays.toString(students));
        Arrays.sort(students);//默认是从小到大排序
        System.out.println(Arrays.toString(students));
    }
复制代码

总结一下就是:如果要进行自定义类型大小的比较,一定要实现可以比较的接口!

但是上面的Comparable接口有一个缺点,如果要换成分数比较代码改动就比较大

缺点:对类的侵入性非常强,一旦写好了,不敢轻易改动!

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 +
                '}';
    }
}

//这里有一个快捷键 alt+7 就是看里面有啥方法!

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

class Score implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return (int)(o1.score - o2.score);//强制转化成int类型
    }
}

//主要使用的接口就是这个!
//同样是今天讲解的第二个常用接口!
class NameComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}

public class TestDemo7 {

    //了解compareTo是如何用的!
   /* public static void main(String[] args) {
        Student student1 = new Student(98,"huahua",56);
        Student student2 = new Student(45,"wewe",34);

        //了解compareTo是如何用的!
//        System.out.println(student1.compareTo(student2));//这里是一个大于零的数字

        //了解compare是如何用的!
        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,"huahua",56);
        students[1] = new Student(45,"wewe",34);
        students[2] = new Student(32,"rous",78);

        System.out.println(Arrays.toString(students));//排序前打印
        AgeComparator ageComparator = new AgeComparator();
        Score score = new Score();
        NameComparator nameComparator = new NameComparator();
        //这里可以使用不同的比较器!
        Arrays.sort(students,nameComparator);//这里ageComparator(传的就是一个比较器),建议看源码!
        System.out.println(Arrays.toString(students));//排序后打印
    }
}
复制代码

所以,我们就可以推导出比较器的好处就是:灵活

对类的侵入性非常弱!

那以后是用Comparable接口还是Comparator接口取决于你的业务,一般推荐比较器!

Cloneable接口及深拷贝和浅拷贝

/**
 * 现在讲第三个接口!
 *
 * 我们继续来探讨一下 创建对象的方式:
 * 1:new关键字
 * 2:实现Cloneable接口
 */

//要想一个类被克隆就要实现Cloneable接口
class Person implements Cloneable{
    public int age;

    public void eat() {
        System.out.println("Eatting!");
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                '}';
    }

    /**
     * clone方法比较特殊,底层是用C/C++实现的,如果要使用它就必须override
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();//这里没有具体的重写实现,其实就是调用的C/C++代码!
    }
}


public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
	Person person = new Person();
    person.age = 99;//这里赋值
    Person person1 = (Person) person.clone();//在内存上,拷贝的age也是99!
        //如果是修改拷贝(person1)的值,也不会影响person的值!
    }

    //决定是深拷贝还是浅拷贝,不是方法所决定的,而是代码的实现!

    //所以说Clone不能说是深拷贝,
    //但是,我们要想办法让它变成深拷贝!
}
复制代码

它的内存图是这样的: 在这里插入图片描述

在说如何深拷贝之前,先了解一下Cloneable接口

我们点开Cloneable接口看看代码在这里插入图片描述 在这里就会牵扯一道面试题:你知道Cloneable接口吗?为啥这个接口是一个空接口?有啥用?

很简单,因为这个接口是空的,所以是空接口

但是它是一个标志接口代表当前类是可以被克隆的!

然后说一下这个接口咋用:

第一次用的时候,它就会报错,需要抛异常解决!

按住ALT+ENTER,点击选项就可以了! 在这里插入图片描述

接着往下看:

/**
 * 现在来说如何深拷贝!
 * 下面将上面的代码,进行升华!
 * 在此之前了解什么是浅拷贝!
 */
class Money implements Cloneable{
    public double m = 12.5;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Person implements Cloneable{
        public int age;
        public Money money = new Money();

        public void eat() {
            System.out.println("Eatting!");
        }

        @Override
        public String toString() {
            return "Person{" +
                    "age=" + age +
                    '}';
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
            //return super.clone();//这里没有具体的重写实现,其实就是调用的C/C++代码!
            Person tmp = (Person)super.clone();
            tmp.money = (Money) this.money.clone();
            return tmp;
            //上面的操作就是一个 深拷贝 的作用!
        }
    }

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person();
        Person person2 = (Person)person.clone();
        System.out.println(person.money.m);
        System.out.println(person2.money.m);
        //打印出来后都是一样的值,为什么呢?看内存图!
        System.out.println("=====================");
        person2.money.m = 99.99;
        System.out.println(person.money.m);
        System.out.println(person2.money.m);
        //打印出来后都是一样的值,为什么呢?看内存图!

        //所以这里是浅拷贝!
        //如何深拷贝呢?

        //决定是深拷贝还是浅拷贝,不是方法所决定的,而是代码的实现!
    }
}
复制代码

它的内存图是这样的: 在这里插入图片描述

如何深拷贝呢?

//就是重写这个类!
 @Override
        protected Object clone() throws CloneNotSupportedException {
//            return super.clone();//这里没有具体的重写实现,其实就是调用的C/C++代码!
            Person tmp = (Person)super.clone();
            tmp.money = (Money) this.money.clone();
            return tmp;
            //上面的操作就是一个 深拷贝 的作用!
        }
    }
复制代码

它的内存图是这样的: 在这里插入图片描述

很重要的一句话:决定是深拷贝还是浅拷贝,不是方法所决定的,而是代码的实现!

5.5 抽象类和接口的区别

抽象类: 一个类被 abstract 修饰,就直接叫抽象类(定义不重要!)

  • 抽象类不能直接实例化(会直接报错)

  • 抽象类中可以有普通的方法和成员

  • 普通类继承了抽象类,这个普通类中必须重写抽象类的所有抽象方法(可以被重写和调用)

  • 抽象方法不能是 private 修饰的

  • 一个抽象类B继承了抽象类A,那么这个抽象类A中可以不实现抽象类A的抽象方法!

  • 在上条继承关系的基础上,普通类C继承了抽象类B,那么A和B中的抽象方法必须被重写!

  • 抽象类和抽象方法是不能被 final 修饰!

接口: 在一个类中,同名的方法如果有不同的参数列表参数类型不同参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,所以不能通过返回类型是否相同来判断重载

  • 接口不能单独被实例化
  • 接口中包含的方法都是抽象方法,字段只能包含静态常量
  • 接口中的普通方法不能有具体的实现,如果要实现,就要使用 default 关键字修饰这个方法!
  • 接口中可以有静态方法,这个静态方法中可以有方法体
  • 接口中的方法一定是抽象方法,因此可以省略 abstract
  • 接口中的方法一定是 public,因此可以省略 public

注意点与区别总结:

  • 都不能被单独实例化
  • 抽象类使用 extends 关键字来继承抽象类;子类使用关键字 implements 来实现接口
  • 抽象方法可以有 publicprotecteddefault 这些修饰符;接口方法默认修饰符是 public,不可以使用其它修饰符。
  • 抽象类只能被单继承;接口可以多实现
  • 抽象类中可以有普通的方法和成员接口中包含的方法都是抽象方法,字段只能包含静态常量

在这里插入图片描述

全文结束

这一篇文章到这里就结束了,期间访问了大量文献,包括各种课件、博客、文档等等!

对了,前面说的一个关于面向对象的训练,链接在左边!

写作实属不易,你们的支持就是我最大的动力!跪求三连!!!

累!

猜你喜欢

转载自juejin.im/post/7066669501697228807