类学习笔记——【类的 封装、继承和多态】

@


源码:
Gitee https://gitee.com/drip123456/java-se
GIthub https://github.com/Drip123456/JavaSE

专栏: JavaSE笔记专栏

封装、继承和多态

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

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

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

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

正是这三大特性,让我们的Java程序更加生动形象。

类的封装

封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个代码带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要使用Getter和Setter方法来查看和设置变量。

我们可以将之前的类进行改进:

public class Person {
    private String name;    //现在类的属性只能被自己直接访问
    private int age;
    private String sex;
  
  	public Person(String name, int age, String sex) {   //构造方法也要声明为公共,否则对象都构造不了
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    public String getName() {
        return name;    //想要知道这个对象的名字,必须通过getName()方法来获取,并且得到的只是名字值,外部无法修改
    }

    public String getSex() {
        return sex;
    }

    public int getAge() {
        return age;
    }
}

我们可以来试一下:

public static void main(String[] args) {
    Person person = new Person("小明", 18, "男");
    System.out.println(person.getName());    //只能通过调用getName()方法来获取名字
}

也就是说,外部现在只能通过调用我定义的方法来获取成员属性,而我们可以在这个方法中进行一些额外的操作,比如小明可以修改名字,但是名字中不能包含"小"这个字:

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

我们甚至还可以将构造方法改成私有的,需要通过我们的内部的方式来构造对象:

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

    private Person(){}   //不允许外部使用new关键字创建对象
    
    public static Person getInstance() {   //而是需要使用我们的独特方法来生成对象并返回
        return new Person();
    }
}

通过这种方式,我们可以实现单例模式:

public class Test {
 private static Test instance;

 private Test(){}

 public static Test getInstance() {
     if(instance == null) 
         instance = new Test();
     return instance;
 }
}

单例模式就是全局只能使用这一个对象,不能创建更多的对象,我们就可以封装成这样,关于单例模式的详细介绍,还请各位小伙伴在《Java设计模式》视频教程中再进行学习。

封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心实现,要用什么由类自己来做,不需要外面来操作类内部的东西去完成,封装就是通过访问权限控制来实现的。

类的继承

前面我们介绍了类的封装,我们接着来看一个非常重要特性:继承。

在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,子类可以使用父类中非私有的成员。

比如说我们一开始使用的人类,那么实际上人类根据职业划分,所掌握的技能也会不同,比如画家会画画,歌手会唱,舞者会跳,Rapper会rap,运动员会篮球,我们可以将人类这个大类根据职业进一步地细分出来:

30724024159.png?origin_url=https%3A%2F%2Fs2.loli.net%2F2022%2F09%2F21%2FzlZ9JXAjvxpawPF.png&pos_id=img-SgrGJ9MA-1706281839192)

实际上这些划分出来的类,本质上还是人类,也就是说人类具有的属性,这些划分出来的类同样具有,但是,这些划分出来的类同时也会拥有他们自己独特的技能。在Java中,我们可以创建一个类的子类来实现上面的这种效果:

public class Person {   //先定义一个父类
    String name;
    int age;
    String sex;
}

接着我们可以创建各种各样的子类,想要继承一个类,我们只需要使用extends关键字即可:

public class Worker extends Person{    //工人类
    
}
public class Student extends Person{   //学生类

}

类的继承可以不断向下,但是同时只能继承一个类,同时,标记为final的类不允许被继承:

public final class Person {  //class前面添加final关键字表示这个类已经是最终形态,不能继承
  
}

当一个类继承另一个类时,属性会被继承,可以直接访问父类中定义的属性,除非父类中将属性的访问权限修改为private,那么子类将无法访问(但是依然是继承了这个属性的):

public class Student extends Person{
    public void study(){
        System.out.println("我的名字是 "+name+",我在学习!");   //可以直接访问父类中定义的name属性
    }
}

同样的,在父类中定义的方法同样会被子类继承:

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

    public void hello(){
        System.out.println("我叫 "+name+",今年 "+age+" 岁了!");
    }
}

子类直接获得了此方法,当我们创建一个子类对象时就可以直接使用这个方法:

public static void main(String[] args) {
    Student student = new Student();
    student.study();    //子类不仅有自己的独特技能
    student.hello();    //还继承了父类的全部技能
}

是不是感觉非常人性化,子类继承了父类的全部能力,同时还可以扩展自己的独特能力,就像一句话说的: 龙生龙凤生凤,老鼠儿子会打洞。

如果父类存在一个有参构造方法,子类必须在构造方法中调用:

public class Person {
    protected String name;   //因为子类需要用这些属性,所以说我们就将这些变成protected,外部不允许访问
    protected int age;
    protected String sex;
    protected String profession;

  	//构造方法也改成protected,只能子类用
    protected Person(String name, int age, String sex, String profession) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.profession = profession;
    }

    public void hello(){
        System.out.println("["+profession+"] 我叫 "+name+",今年 "+age+" 岁了!");
    }
}

可以看到,此时两个子类都报错了:

在这里插入图片描述

因为子类在构造时,不仅要初始化子类的属性,还需要初始化父类的属性,所以说在默认情况下,子类其实是调用了父类的构造方法的,只是在无参的情况下可以省略,但是现在父类构造方法需要参数,那么我们就需要手动指定了:

既然现在父类需要三个参数才能构造,那么子类需要按照同样的方式调用父类的构造方法:

public class Student extends Person{
    public Student(String name, int age, String sex) {    //因为学生职业已经确定,所以说学生直接填写就可以了
        super(name, age, sex, "学生");   //使用super代表父类,父类的构造方法就是super()
    }

    public void study(){
        System.out.println("我的名字是 "+name+",我在学习!");
    }
}
public class Worker extends Person{
    public Worker(String name, int age, String sex) {
        super(name, age, sex, "工人");    //父类构造调用必须在最前面
        System.out.println("工人构造成功!");    //注意,在调用父类构造方法之前,不允许执行任何代码,只能在之后执行
    }
}

我们在使用子类时,可以将其当做父类来使用:

public static void main(String[] args) {
    Person person = new Student("小明", 18, "男");    //这里使用父类类型的变量,去引用一个子类对象(向上转型)
    person.hello();    //父类对象的引用相当于当做父类来使用,只能访问父类对象的内容
}

虽然我们这里使用的是父类类型引用的对象,但是这并不代表子类就彻底变成父类了,这里仅仅只是当做父类使用而已。

我们也可以使用强制类型转换,将一个被当做父类使用的子类对象,转换回子类:

public static void main(String[] args) {
    Person person = new Student("小明", 18, "男");
    Student student = (Student) person;   //使用强制类型转换(向下转型)
    student.study();
}

但是注意,这种方式只适用于这个对象本身就是对应的子类才可以,如果本身都不是这个子类,或者说就是父类,那么会出现问题:

public static void main(String[] args) {
    Person person = new Worker("小明", 18, "男");   //实际创建的是Work类型的对象
    Student student = (Student) person;
    student.study();
}

在这里插入图片描述

此时直接出现了类型转换异常,因为本身不是这个类型,强转也没用。

那么如果我们想要判断一下某个变量所引用的对象到底是什么类,那么该怎么办呢?

public static void main(String[] args) {
    Person person = new Student("小明", 18, "男");
    if(person instanceof Student) {   //我们可以使用instanceof关键字来对类型进行判断
        System.out.println("对象是 Student 类型的");
    }
    if(person instanceof Person) {
        System.out.println("对象是 Person 类型的");
    }
}

如果变量所引用的对象是对应类型或是对应类型的子类,那么instanceof都会返回true,否则返回false

最后我们需要来特别说明一下,子类是可以定义和父类同名的属性的:

public class Worker extends Person{
    protected String name;   //子类中同样可以定义name属性
    
    public Worker(String name, int age, String sex) {
        super(name, age, sex, "工人");
    }
}

此时父类的name属性和子类的name属性是同时存在的,那么当我们在子类中直接使用时:

public void work(){
    System.out.println("我是 "+name+",我在工作!");   //这里的name,依然是作用域最近的哪一个,也就是在当前子类中定义的name属性,而不是父类的name属性
}

所以说,我们在使用时,实际上这里得到的结果为null

在这里插入图片描述

那么,在子类存在同名变量的情况下,怎么去访问父类的呢?我们同样可以使用super关键字来表示父类:

public void work(){
    System.out.println("我是 "+super.name+",我在工作!");   //这里使用super.name来表示需要的是父类的name变量
}

这样得到的结果就不一样了:

在这里插入图片描述

但是注意,没有super.super这种用法,也就是说如果存在多级继承的话,那么最多只能通过这种方法访问到父类的属性(包括继承下来的属性)

posted @ 2024-02-28 22:24  笠大  阅读(22)  评论(0编辑  收藏  举报