【JavaSE】面向对象编程必备技能,你学会了吗(继承、多态、抽象类、接口详解)

在这里插入图片描述
希望通过博客和大家相互交流,相互学习,如有错误,请评论区指正

面向对象编程

一、包

在一个文件夹中不能创建两个文件名相同的文件

当一个大型程序交由数个不同的程序开发人员进行开发时,用到相同的类名是很有可能的,在java程序开发中为了避免上述事件,提供了一个包的概念(package),使用方法很简单,只需要在写的程序第一行使用 package 关键字来声明一个包。

包是组织类的一种方式,使用包就是为了保证类的唯一性

之前我们在用Scanner时就需要导java.util.Scanner这个包,用import关键字(导入系统包),那么我们在导自己的包的时候就用的是package

java.lang这个包下的所有东西都不需要进行手动导入

OOP语言三大特征:继承,封装,多态

二、继承

Java中使用了类就是为了将现实中的一些事务进行抽象,有时现实中的一些事务直间存在着一些关联,那么我们在创建类的时候就可以让他们有关联

举例

比如动物,狗,小鸟, 现在创建他们的类:

在三个不同的java文件中

public class Animal {
    
    
    public String name;
    public int age;
    public void eat() {
    
    
        System.out.println("Animal->eat");
    }
}
public class Bird {
    
    
    public String name;
    public int age;
    public void eat() {
    
    
        System.out.println("Bird->eat");
    }
    public void fly() {
    
    
        System.out.println("Bird->fly");
    }
}
public class Dog {
    
    
    public String name;
    public int age;
    public void eat() {
    
    
        System.out.println("Dog->eat");
    }
    public void run() {
    
    
        System.out.println("Dog->run");
    }
}

我们会发现它们有部分共同的属性:name,age,当然也有一部分他们各自所特有的属性,相同的属性如果每个类都去定义一遍就感觉有好多部分都在重复,里面的类似的方法也在重复(eat方法),代码冗余

这几个类之间都一定的关联关系:

  1. 这几个类都具有name和age属性,而且意义相同
  2. 这几个类都有eat方法,行为相同
  3. Bird和Dog都属于Animal

因为有这样的关联关系,我们就可以让 Bird 和 Dod 继承 Animal,减少代码冗余,简化代码,逻辑更清晰,并达到代码复用的效果

那么到底什么是继承?

继承是面向对象最显著的一个特性。继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力

Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类

这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。

比如可以先定义一个类叫车,车有以下属性:车体大小,颜色,方向盘,轮胎,而又由车这个类派生出轿车和卡车两个类,为轿车添加一个小后备箱,而为卡车添加一个大货箱。

被继承的类称为基类超类父类,继承的类称为派生类子类

基本语法
class 子类 extends 父类 {
    
    
    
}

注意:

  • 继承关键字: extends
  • 单继承:一个子类只能继承一个父类
  • 父类private的字段和方法子类无法访问
  • 子类在构造的时候要先帮助父类进行构造
  • 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用
super关键字

super代表父类对象的引用(和this语法相同),有以下应用:

  • 调用父类的构造方法 (必须放到第一行)
  • 访问父类的属性
  • 调用父类的成员方法

子类到底继承了父类的什么?

子类继承了父类的除了构造方法外的一切

将上面的例子写成继承的形式

public class Animal {
    
    
    public String name;
    public int age;
    public void eat() {
    
    
        System.out.println("Animal->eat");
    }
}
public class Bird extends Animal{
    
    
    public void fly() {
    
    
        System.out.println("Bird->fly");
    }
}
public class Dog extends Animal {
    
    
    public void run() {
    
    
        System.out.println("Dog->run");
    }
}

这样继承之后,Bird里面虽然没写name和age,以及eat方法,但是因为它继承了父类Animal,所以它也具有父类的属性,也有父类的eat,Dog同理

访问限定修饰符

范围 private default(包访问权限) protected public
1 同一个包中的同一个类
2 同一个包中不同类
3 不同包中的子类
4 不同包中的非子类

复杂的继承

在情况比较复杂的情况下,我们可能还需要复杂的继承来实现目的,如下:


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

虽然继承很方便,但我们在实际应用中类之间的继承方式不能太过复杂,否则代码可读性就会降低,一般不希望出现超过三层的继承关系

可以用final来修饰类,此时这个类就不能被继承了

三、多态

向上转型

向上转型就是将子类对象赋值给父类引用, (父类引用 引用 子类对象)

如下图所示:

Dog dog = new huntaway();   // 父类引用 引用 子类对象

注意:

通过父类的引用只能访问父类自己的方法或属性

发生向上转型的时机

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

动态绑定 / 运行时绑定

父类引用 引用 子类对象,并且子类对象中有和父类同名的方法,当用父类引用去调用同名构造方法的时候就会发生运行时绑定,用到的是子类中的同名方法

为什么叫运行时绑定?

我们通过以下代码来看看

// 三段代码在三个java文件中

public class Animal {
    
    
    public String name;
    public int age;
    public Animal(String name) {
    
    
        this.name = name;
        System.out.println(this.name);
    }
    public void eat() {
    
    
        System.out.println("Animal->eat");
    }
}
public class Dog extends Animal{
    
    
    public int aaa;
    public Dog(String name) {
    
    
        super(name);
    }
    public void eat() {
    
    
        System.out.println(this.name + "->eat");
    }
}
public class Demo {
    
    
    public static void main(String[] args) {
    
    
        Animal dd = new Animal("大大");
        dd.eat();
        Animal xx = new Dog("小小");
        xx.eat();
    }
}

运行结果

通过反编译看看

javac Demo.java   // 编译
javap -c Demo     // 反编译Java代码

反编译结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p379iHR2-1641631903743)(C:\Users\lycre\AppData\Roaming\Typora\typora-user-images\image-20220106175254464.png)]
我们会发现在编译时第二次调用eat()的时候仍然调用的是Animal类中的方法,但是执行出来的结果却是执行Dog类中的eat()产生的结果,这就是因为发生了运行时绑定

注意:

  • 编译器检查有哪些方法,看的是Animal这个类型
  • 执行时,究竟执行的是父类方法还是子类方法,看的是Dog类型
  • 构造方法中也会发生运行时绑定(当创建子类对象调用子类构造方法时,会先调用父类的构造方法,而如果父类的构造方法中又调用了父类和子类中同名的方法,就会发生运行时绑定,实际执行的是子类中的重写方法)

方法的重写

刚刚上面的eat方法其实就是重写

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

  1. 方法名相同
  2. 参数列表相同
  3. 返回值类型相同
  4. 父类有同名的方法

重写方法需要注意:

  1. 需要被重写的方法不能被final修饰,被final修饰的方法称为密封方法,不可修改
  2. 被重写的方法,访问限定符不能是private
  3. 被重写的方法和重写的方法的访问限定符可以不一样,子类当中重写的方法的访问权限要 >= 父类中被重写的方法的访问权限
  4. static修饰的静态方法不能被重写

向下转型

向下转型就是子类引用 引用 父类对象

如下代码示例:

public class Animal {
    
    
    public String name;
    public int age;
    public Animal(String name) {
    
    
        this.name = name;
        System.out.println(this.name);
    }
    public void eat() {
    
    
        System.out.println("Animal->eat");
    }
}
public class Dog extends Animal{
    
    
    public int aaa;
    public Dog(String name) {
    
    
        super(name);
    }
    public void eat() {
    
    
        System.out.println(this.name + "->eat");
    }
    public void run() {
    
    
        System.out.println("dog->run");
    }
}
public class Demo {
    
    
    public static void main(String[] args) {
    
    
        Animal dd = new Dog("小小");
        dd.eat();
        Dog dog = (Dog)dd;
        dog.run();
    }
}

运行结果:

这就是向下转型

但是我们应该要注意,向下转型是非常不安全的,通常不建议使用

因为有可能Animal本身引用的是Dog类型的对象,结果想转成cow类型,这就有问题了,Dog类型是不能转成cow类型的,我们再想将Animal转回去的话就应该先判断一下Animal本质上是不是cow类型,再决定要不要转

可以用运算符instanceof来判断该引用是否为某个类的实例化对象

Animal animal = new Dog("小小");
if (animal instanceof Cow) {
    
    
    Cow cow = (Cow)animal;
}

如果类型不匹配的话就会抛出类型转换异常ClassCastException

多态

多态就是当父类引用 引用 子类对象,并且父类和子类中有同名的构造方法,那么两个父类引用去调用同名构造方法时,产生不同的效果

如下代码示例:

public class Animal {
    
    
    public String name;
    public int age;
    public void eat() {
    
    
        System.out.println("Animal->eat");
    }
}
public class Dog extends Animal{
    
    
    public int aaa;
    public void eat() {
    
    
        System.out.println("dog->eat");
    }
}
public class Bird extends Animal{
    
    
    public void eat() {
    
    
        System.out.println("bird->eat");
    }
}

public class Demo {
    
    
    public static void main(String[] args) {
    
    
        Animal animal = new Dog();
        Animal animal1 = new Bird();
        animal.eat();
        animal1.eat();
    }
}

运行结果:

虽然调用的都是eat,但是却不是同一个eat

四、抽象类

在实际开发中,如果父类中的方法并没有什么实际的执行逻辑,而是希望用到子类中同名的重写方法来完成需求,那么我们就可以把它设计成一个抽象方法(abstract method),包含抽象方法的类称为抽象类(abstract class).

如下:

abstract class parent {
    
    
    public abstract void func();
}
  • 抽象方法没有方法体,不执行具体代码
  • 包含抽象方法的类,就得用abstract修饰,称为抽象类

注意:

  1. 抽象类不能被实例化
  2. 抽象类中的数据成员和普通类没有区别(除了多一个抽象方法外,与其他类基本没有区别,也可以包含字段,非抽象方法)
  3. 抽象类主要就是用来被继承的
  4. 如果一个类继承了抽象类,那么这个类就必须要重写抽象类中的抽象方法
  5. 抽象类 A 继承 了抽象类B,那么A可以不重写B中的方法,但A如果再被继承,继承A的那个类还是要重写这个抽象方法的
  6. 抽象类或抽象方法不能被final修饰
  7. 抽象类不能被private修饰

五、接口

详解

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

如下:

interface Animal {
    
    
    public static final int a = 10;
    public abstract void func();
}

这里的public static final 和 public abstract 只是为了说明问题,实际开发中建议省略掉

  • 接口用关键字interface来修饰
  • 接口不能被实例化(为了解决多继承问题)
  • 接口中的方法都是抽象方法(可以省略abstract), 并且这些方法都是public的(public可以省略)
interface Animal {
     
     
    int a = 5;
    void func();
}
  • 接口当中其实也可以有具体实现的方法,这个方法是被default修饰的 JDK1.8加入
interface Animal {
     
     
    default void func() {
     
     
        System.out.println("具体实现的方法");
    }
}
  • 接口中的成员变量默认是public static final,接口中的方法public abstract
  • 一个类继承一个接口,此时不是“扩展”,而是“实现”(implements)
public class Dog implements Animal {
     
     
    
}

接口的含义其实就是让其具有某种特性

cloneable接口和深拷贝

首先来看看之前在拷贝数组时用到的clone()方法

import java.util.Arrays;
public class CloneableDemo {
    
    
    public static void main(String[] args) {
    
    
        int[] arr = {
    
    1,2,3,4,5,6};
        int[] arr2 = arr.clone();
        arr2[0] = 666;
        System.out.println(Arrays.toString(arr));
        System.out.println(Arrays.toString(arr2));
    }
}

运行结果:

在这个过程中,通过arr2来修改第一个下标的值,对arr里面的数值并未产生影响,可以看到通过clone()方法也实现了对数组的拷贝,但其实clone()方法是一个浅拷贝

浅拷贝

例如arr里面放的是引用类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-phkHRcG3-1641631903744)(C:\Users\lycre\AppData\Roaming\Typora\typora-user-images\image-20220108154147281.png)]
如果arr里面放的是引用类型的话,那么通过clone()方法只拷贝了对象的引用,并未拷贝对象,造成藕断丝连的情况,只是浅拷贝

而我们想要达到的效果应该是下图这样的深拷贝

深拷贝


如果想用clone()方法克隆自定义类型,就需要实现接口

实现接口

但是当我们去实现Cloneable接口时,却发现Cloneable接口是一个空接口

为什么是空接口

Cloneable接口在源码当中是没有抽象方法的

空接口:也把它叫做标记接口,只要一个类实现了这个接口,那么就标记这个类是可以进行clone的,然后在这个类中重写clone方法就行了

public class Person implements Cloneable {
    
    
    public String name;
    public int age;

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

欢迎大家关注!!!

一起学习交流 !!!

让我们将编程进行到底!!!

--------------原创不易,请三连支持-----------------

猜你喜欢

转载自blog.csdn.net/weixin_46531416/article/details/122382743