类的封装继承和多态

** **封装、继承和多态是面向对象编程的三大编程。

封装,把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。

继承,从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。

多态,多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的方法。

类的封装

** **封装的目的是为了保障变量的安全性,使得使用者在使用的时候不必要在意实现的细节,只需要通过外部接口来访问类的成员,如果不进行封装,那么就可以通过创建类实列来创建实例,可能会修改影响到其他代码,所以说在编写类时就会将成员变量私有化,在外部通过Getter和Setter来查看和设置即可;

** ⭐️将Cat类中的成员变量变为**private,这样在外部就访问不到了

 package com.company;
 ​
 import sun.awt.windows.WPrinterJob;
 ​
 public class Cat {
     private String name;
     private String sex;
     private int age;
     private static  String info;
 ​
     public Cat(String name,String sex,int age){
         this.name = name;
         this.sex = sex;
         this.age = age;
     }
 }

image-20240326110623154

这样在外部就不能直接通过类对象来获取和修改,但是可以使用get和set来获取和修改

 package com.company;
 ​
 import sun.awt.windows.WPrinterJob;
 ​
 public class Cat {
     private String name;
     private String sex;
     private int age;
     private static  String info;
 ​
     public Cat(String name,String sex,int age){
         this.name = name;
         this.sex = sex;
         this.age = age;
     }
 ​
     public String getName() {
         return name;
     }
 ​
     public void setName(String name) {
         this.name = name;
     }
 ​
     public String getSex() {
         return sex;
     }
 ​
     public void setSex(String sex) {
         this.sex = sex;
     }
 ​
     public int getAge() {
         return age;
     }
 ​
     public void setAge(int age) {
         this.age = age;
     }
 ​
     public static String getInfo() {
         return info;
     }
 ​
     public static void setInfo(String info) {
         Cat.info = info;
     }
 }
 ​

image-20240326111023103

⭐️也可以通过修改set函数,对外界修改进行一些额外的操作,如下,使得外界调用修改 name时不能修改包含有"猪"这个字

     public void setName(String name) {
         if(name.contains("猪")) return;
         this.name = name;
     }

可以看到修改失败

image-20240326190311826

⭐️还可以限制构造方法,将构造方法改为私有,只能通过内部的方式来构造对象

 public class Cat {
     private String name;
     private String sex;
     private int age;
     private static  String info;
 ​
     private Cat() {
 ​
     }
 ​
     public static  Cat create(){
         return new Cat();
     }
 ​
     public String getName() {
         return name;
     }

image-20240326190907648

类的继承

** **有时候定义很多类,它们都会有相同的属性,我们可以将这些相同的属性共同组成一个父类,子类可以继承自父类但是不能访问父类的私有成员变量

** **类的继承就相当于你和你老爸,你的很多是不是都是继承你的老爸的,性格啊、外貌啊、等等,当然有些东西是你爸爸自己独有的你继承不了,比如说你爸爸的年龄你肯定继承不了吧

** **还有一种就是人和我们自己,每个人的相同特征是不是都是有一个名字,有年龄,还有性别,这是人类共有的,但是你也会有属于你自己的东西,比如说你的职业,你的性格等等,就可以看作是两个类,一个类叫做人,一个类叫做学生,学生是继承人的特性的,那么学生也会有自己的成员变量,比如在哪里读书,成绩怎么样等等

这是一个父类

 public class Person{
     String name;
     String sex;
     int age;
 }

⭐️可以通过extends关键字来继承父类,创建了一个学生类继承了 Person

 public class Student extends Person {
     
 }

⭐️类可以不断继续向下衍生,但是每个子类只能继承一个类,如果不想这个类被继承需要将类标记为 final

 package com.company;
 ​
 public final class Person{
     String name;
     String sex;
     int age;
 }
 ​

image-20240326192638606

前面我们说过,子类无法访问到父类的私有变量,但是是已经继承了但是无法访问

⭐️如果父类有一个全参构建方法,需要参数才可以创建对象,那么在子类中是无法直接继承的

 public  class Person{
     String name;
     String sex;
     int age;
 ​
     public Person(String name, String sex, int age) {
         this.name = name;
         this.sex = sex;
         this.age = age;
     }
 }

image-20240326193524129

** ⭐️因为子类在构建的时候,不仅仅要初始化子类的属性,还需要初始化父类的属性,在默认的情况下子类调用了父类的构建方法,但是如果在全参的情况下,就需要手动指定使用**super,super就表示父类 this表示当前类

 public class Student extends Person {
 ​
     public Student(String name, String sex, int age) {
         super(name, sex, age);
     }
 }
 ​

** **我们在使用子类时,是可以当作父类去使用的,在父类定义一个 hello方法

 package com.company;
 ​
 public  class Person{
     String name;
     String sex;
     int age;
 ​
     public void hello() {
         System.out.println("我是父类的Hello!");
     }
 }
 ​

这里我用父类的类型,去应引用了一个子类的类型,但是这个子类的类型是访问到父类的东西的

image-20240326195502737

⭐️但是这里不是直接变成父类了,仅仅是当作父类使用而已,可以使用强制类型转换来转换成子类(但是我觉得并没有任何意义)

 public class Main {
 ​
     public static void main(java.lang.String[] args) {
     // write your code here
         Person person = new Student();
         person.hello();
         Student student = (Student) person;
         student.hello();
     }
 }

⭐️如果我们需要判断这个变量所引用的对象到底是什么类可以使用 instanceof来判断

 public class Main {
 ​
     public static void main(java.lang.String[] args) {
     // write your code here
         Person person = new Student();
         if (person instanceof Person) {
             out.println("是Person类");
         }
         if (person instanceof Student) {
             out.println("是Student类");
         }
     }
 }
 ​

⭐️如果子类和父类存在于同名的成员变量,那么就可以使用到 super来访问

 package com.company;
 ​
 public  class Person{
     public String name = "白白白";
     String sex;
     int age;
 ​
     public void hello() {
         System.out.println("我是父类的Hello!");
     }
 }
 ​

⭐️子类在子类中使用 super来调用父类的名字

package com.company.zy01;

import com.company.Person;

public class Student extends Person {
    String name="黑黑黑";

    public void outputString() {
        System.out.println("我的父类叫"+super.name);
        System.out.println("我叫"+this.name);
    }

}

输出image-20240326201050695

顶层Object类

⭐️所有的类默认都是默认继承了 Object类,可以看到提示冗余

image-20240326202136295

如下图所示,加粗的都是类本身自带的方法,而不加粗的都是继承自 Object

image-20240326202339054

可以来看看这个类里面有哪些内容

 public class Object {
 ​
     private static native void registerNatives();   //标记为native的方法是本地方法,底层是由C++实现的
     static {
         registerNatives();   //这个类在初始化时会对类中其他本地方法进行注册,本地方法不是我们SE中需要学习的内容,我们会在JVM篇视频教程中进行介绍
     }
 ​
     //获取当前的类型Class对象,这个我们会在最后一章的反射中进行讲解,目前暂时不会用到
     public final native Class<?> getClass();
 ​
     //获取对象的哈希值,我们会在第五章集合类中使用到,目前各位小伙伴就暂时理解为会返回对象存放的内存地址
     public native int hashCode();
 ​
     //判断当前对象和给定对象是否相等,默认实现是直接用等号判断,也就是直接判断是否为同一个对象
     public boolean equals(Object obj) {
         return (this == obj);
     }
   
     //克隆当前对象,可以将复制一个完全一样的对象出来,包括对象的各个属性
     protected native Object clone() throws CloneNotSupportedException;
 ​
     //将当前对象转换为String的形式,默认情况下格式为 完整类名@十六进制哈希值
     public String toString() {
         return getClass().getName() + "@" + Integer.toHexString(hashCode());
     }
 ​
     //唤醒一个等待当前对象锁的线程,有关锁的内容,我们会在第六章多线程部分中讲解,目前暂时不会用到
     public final native void notify();
 ​
     //唤醒所有等待当前对象锁的线程,同上
     public final native void notifyAll();
 ​
     //使得持有当前对象锁的线程进入等待状态,同上
     public final native void wait(long timeout) throws InterruptedException;
 ​
     //同上
     public final void wait(long timeout, int nanos) throws InterruptedException {
         ...
     }
 ​
     //同上
     public final void wait() throws InterruptedException {
         ...
     }
 ​
     //当对象被判定为已经不再使用的“垃圾”时,在回收之前,会由JVM来调用一次此方法进行资源释放之类的操作,这同样不是SE中需要学习的内容,这个方法我们会在JVM篇视频教程中详细介绍,目前暂时不会用到
     protected void finalize() throws Throwable { }
 }

方法的重写

** **方法的重写是为了将之前原本的方法实现,来重写方法,使得达到自己想要的效果,上面我们提到过,任何类都会继承Object类,那我们可以使用重写的方法来修改一下这个父类的方法,接下来我们使用代码来重写一下 equals方法,在 Object中提到了 equals方法是检测两个对象是否相等

image-20240326205313098

** **但是如果是以下情况就不能,他们两名字一样,性别一样,年龄也一样,我们可以重写 equals方法是他判断成员变量是否相等,如果相等就表示他们相等

image-20240326205521544

重写方法,在 Person类中

 public class Person{
     ...
 ​
     @Override   //重写方法可以添加 @Override 注解,有关注解我们会在最后一章进行介绍,这个注解默认情况下可以省略
     public boolean equals(Object obj) {   //重写方法要求与父类的定义完全一致
         if(obj == null) return false;   //如果传入的对象为null,那肯定不相等
         if(obj instanceof Person) {     //只有是当前类型的对象,才能进行比较,要是都不是这个类型还比什么
             Person person = (Person) obj;   //先转换为当前类型,接着我们对三个属性挨个进行比较
             return this.name.equals(person.name) &&    //字符串内容的比较,不能使用==,必须使用equals方法
                     this.age == person.age &&       //基本类型的比较跟之前一样,直接==
                     this.sex.equals(person.sex);
         }
         return false;
     }
 }

再次使用就是返回 true

image-20240326210216547

⭐️在类中,有时候为了方便我们查看各个成员变量对应的值,就可以重写 toString方法,这个是可以通过 ide快捷生成的

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

image-20240326210448436

⚠️ 静态类是不能被重写但是可以被继承

** **基于这种方法,在一个父类中定义了一个行为(方法),不同的子类会表现出不同的方法,比如在人这个类中定义了一个收入,学生和工人的收入应该是会不一样的,所以说: 对于一个类的行为,不同的子类可以出现不同的行为

这其实就是多态特性的一种体现

 public  class Person {
     ....
     public  void income(){ //定义了收入行为
     }
 }
 ​

在两个类中重写 income方法

     @Override
     public void income() {
         System.out.println("我是学生,我在学习我没有收入");
     }
     
     
     @Override
     public void income() {
         System.out.println("我是工人,我每个月有4000的工资");
     }

Main函数调用

image-20240326213103589

⭐️那么如果不希望重写这个方法,就可以假如 final关键字,表示这个方法已经是最终形态了;

     public final  void income(){
     }

image-20240326213304372

** ⭐️还有一种方法就是将父类的方法直接设置为private,那么子类也同样无法访问,也不能重写了,但是子类可以不加注解,直接创建一个新的同名方法**

 public  class Person {
     ....
     private void income(){
     }
 }

image-20240326213516070

Override注解去除

image-20240326213550147

image-20240326213604205

⭐️如果需要在子类中调用父类的方法,还是可以使用 super字段

     @Override
     public void income() {
         super.income();
         System.out.println( "我也是学生,我在学习我没有收入");
     }
 }

image-20240326214108266

⭐️子类的重写权限可以别父类大,但是不能比父类低

     protected void income(){ //父类中的方法是protected权限
         System.out.println("我是人类");
     }

修改子类中的方法,可以看到如果我们将子类变成默认权限,就会提示权限报错

image-20240326214407373

抽象类

⭐️在Java中,抽象是指无法直接实例化的类和没有具体实现的方法

  • 抽象类(Abstract Class):抽象类是一种特殊的类,它不能被直接实例化。它的目的是为了被其他类继承,并提供一组公共的接口或部分实现供子类使用或扩展。抽象类通常包含抽象方法和具体方法:
    • 抽象方法:在抽象类中定义但没有具体实现的方法,它们只有声明而没有方法体。这些方法必须在任何直接或间接继承该抽象类的子类中被重写(实现)。
    • 具体方法:抽象类也可以包含已经实现了的具体方法,这些方法可以在子类中直接使用,或者根据需要进行重写。
  • 抽象方法(Abstract Method):抽象方法是一种没有实现的方法,只有方法签名,即方法名、参数列表和返回类型。它们必须在继承了抽象类的任何非抽象子类中被实现。

⭐️之前我们说过多态就是一个父类里面有一个行为方法,但是在继承它的不同子类的方法就会有不同的结果,这个行为方法是一点会被重写在子类中实现的,这个行为方法完全就是可以在子类中进行实现,父类不需要提供实现的,就可以使用抽象类,使用 abstract定义为抽象类

 public abstract class Person { //将Person类定义为抽象类
 ​
 }

⭐️ 抽象类和普通类的区别在于,抽象类可以拥有抽象方法,前面说过抽象方法不需要在父类中去实现,而是要在子类中去实现,所以说抽象类中的抽象方法是不能有主体的;

 public abstract class Person { //将Person类定义为抽象类
     public abstract void income(); //定义抽象类
 }

image-20240327105335343

⭐️ 子类如果继承的是一个抽象类,则必须实现抽象类中的抽象方法

 public class Worker extends Person {
     @Override
     public void income() { //实现抽象方法
         System.out.println("我也是是工人,我每个月有4000的工资");
     }
 }

image-20240327105533746

⭐️ 抽象类不同于普通类可以直接创建类对象,因为抽象类可能会存在某些方法没有实现(比如抽象方法需要在子类中实现),所以说无法直接通过 new来创建对象,所以说要使用抽象类就需要使用子类来创建

image-20240327105728399

image-20240327105830496

⭐️ 抽象类一般是做继承使用,子类也可以是一个抽象类,如果子类也是一个抽象类,那么它可以不去实现父类的抽象方法,可以看到下图并没有报错

image-20240327110054614

⭐️ 抽象类中不是只能由抽象方法,也可以有正常方法

 public abstract class Student extends Person {
 ​
     public void hello(){
         System.out.println("Hello 我是Student类的正常方法");
     }
 ​
 }

⭐️ 抽象方法不能为 private,抽象方法一定是需要子类实现的,如果子类都实现不了,那就没有什么意义了

image-20240327110407863

接口

** **接口和类不一样,它是一些方法的集合,接口包含了一类方法的定义,类可以实现这个接口,表示类支持接口代表的功能(类似于一个插件,只能作为一个附属功能加在主体上,同时具体实现还需要由主体来实现)

** **比如对于人的不同的职业,比如老师和学生,他们都具有学习的能力,就可以将学习作为一个结构来进行使用,只要是这个接口的类就都回有学习的能力

 public interface Study {
     public void  study(); //接口内的方法也不可以创建本体,具体和抽象方法类似,但是可以使用默认方法,在下面会说到
 }

之后就可以让类来调用这个接口了(需要在创建一个 Teacher类),使用关键字 implement来继承类,⭐️如果接口类有方法的话,接口和继承抽象类拥一样必须需要实现否则会报错,如果不想实现,可以将类设置为抽象类,或者

 public class Teacher implements Study{
 }

image-20240327194012245

 public class Teacher implements Study{
     @Override
     public void study() {
         System.out.println("我是老师,我会在教书的过程中边教边学");
     }
 }
 ​
 ​
 public  class Student extends Person implements Study{
 ​
     @Override
     public void study(){
         System.out.println("我是学生,我在读书的过程中学习");
     }
 }

⭐️接口不同于继承,接口可以连接多个

 public interface Run { //在创建一个Run接口
     public void run();
 }

学生类在继承一个 Run接口

 public  class Student extends Person implements Study,Run{
 ​
     @Override
     public void study(){
         System.out.println("我是学生,我在读书的过程中学习");
     }
 ​
     public void run(){
         System.out.println("我会跑步");
     }
 }
 ​

⭐️ 学到这里我觉得这个和继承差不多,但是要比继承更加强大,如果是普通继承的话,就不是必须要实现父类中的行为,而抽象类和接口则必须实现,但是接口可以连接多个,而继承则只能继承一个

⭐️ 接口和抽象类一样不可以直接创建对象,但是可以将接口的实现类的对象(就是连接了接口的类),以接口的形式去使用

image-20240327200134216

⭐️ 接口也支持向连接了它的类对象强转,如下代码所示,但是我觉得非常多余,当然只需要知道就好

 public class Main {
 ​
     public static void main(String[] args) {
     // write your code here
         Study stu = new Student();
         if(stu instanceof Student){
             Student s1 = (Student) stu;
             stu.study();
         }
     }
 ​
 }

⭐️Java8开始,接口就可以存在方法的默认实现,默认方法就不需要强制要求实现

 public interface Run {
     public void run();
     default void test1(){
         System.out.println("我是默认实现方法1");
     }
     default void test2(){
         System.out.println("我是默认实现方法2");
     }
 }
 public class Main {
 ​
     public static void main(String[] args) {
     // write your code here
         Student stu = new Student();
         Teacher tea = new Teacher(); //这里需要将Teacher类连接Run接口
         stu.test1();
         tea.test2();
     }
 }
 ​

⭐️ 接口不允许存在成员变量和成员方法,但是可以存在静态变量和静态方法

 public interface Study {
     String course_1 = "物理"; //我这里没有写static,默认就是static
     
     public void  study();
         public static void sta(){
         System.out.println("我是接口的静态方法");
     }
 }

可以看到默认就是 public static final

image-20240327201904563

和普通的类中的一样,可以直接通过接口名来调用

 public class Main {
 ​
     public static void main(String[] args) {
     // write your code here
         Study.sta();
         System.out.println("Study.course_1");
 ​
     }
 }

image-20240327202500526

⭐️ 接口是可以继承于接口的,但是接口的继承可以继承多个,接口继承接口相当于两个接口的继承

 public interface Run extends Study{
     ...
 }

⭐️ 在上面提到 object类提供了一个克隆方法,它是需要接口才可以使用的,需要创建 Cloneable接口

 public interface Cloneable { //不需要定义任何方法
     
     
 }

由类连接接口

 public  class Student extends Person implements Study,Run,Cloneable{ //连接接口
 ​
 ​
 }

还需要将 clone方法的权限范围修改一下,这就需要使用到重写

 package com.company.zy01;
 ​
 public  class Student extends Person implements Study,Run,Cloneable{
     @Override
     public Object clone() throws CloneNotSupportedException {   //提升clone方法的访问权限
         return super.clone();   //因为底层是C++实现,我们直接调用父类的实现就可以了
     }
 }

然后使用,报错了

image-20240327215101166

枚举类

一个学生肯定会有,它的一个状态的,比如睡觉,吃饭,学习这几个状态,那么这个状态就可以被定义为一个枚举类,主要是枚举类它可以提示你支持什么;

 public  class Student extends Person implements Study,Run,Cloneable{
     private  String status ;
  
  
  }

然后创建这个成员变量的枚举类

 package com.company.zy01;
 ​
 public enum Status {
     STUDY,SLEEP,EAT
 }
 ​

这样就可以知道我们这个成员变量支持哪些参数了

image-20240327221348523

枚举类型使用起来就非常方便了,其实枚举类型的本质就是一个普通的类,但是它继承自 Enum类,我们定义的每一个状态其实就是一个 public static final的Status类型成员变量:

 //这里使用javap命令对class文件进行反编译得到 Compiled from "Status.java"
 public final class com.test.Status extends java.lang.Enum<com.test.Status> {
   public static final com.test.Status RUNNING;
   public static final com.test.Status STUDY;
   public static final com.test.Status SLEEP;
   public static com.test.Status[] values();
   public static com.test.Status valueOf(java.lang.String);
   static {};
 }   

⭐️ 也可以给枚举的普通类

 public enum Status {
     RUNNING("睡觉"), STUDY("学习"), SLEEP("睡觉");   //无参构造方法被覆盖,创建枚举需要添加参数(本质就是调用的构造方法)
 ​
     private final String name;    //枚举的成员变量
     Status(String name){    //覆盖原有构造方法(默认private,只能内部使用!)
         this.name = name;
     }
 ​
     public String getName() {   //获取封装的成员变量
         return name;
     }
 }

这样,枚举就可以按照我们想要的中文名称打印了:

 public static void main(String[] args) {
     Student student = new Student("小明", 18, "男");
     student.setStatus(Status.RUNNING);
     System.out.println(student.getStatus().getName());
 }