java类和对象:继承、多态、接口、抽象类

目录😊

😘😘🚀点击传送

一、类的继承

        1、什么是继承

        2、如何继承

        1、关键字extends 

        2、构造方法

        3、重写

        3、Object类

        Obejct类中方法:

 二、多态

三、抽象类

四、接口类

五、final关键字

1、final防止重写

2、使用final放置继承

3、final修饰的变量



前言:继承和多态是面向对象开发的重要环节,使用得当可以让代码的的功能更加灵动、高效,同时还可以减少代码的冗余。


一、类的继承

       

        1、什么是继承

        例如,现在有这里有一些动物:鸡,鸭,猪,狗等,他们都属于一个动物类(Animal)。根据java类的基础特性我们可以知道,类可以抽象出来实例化成为一个个具体的对象,但是他们都有很多共同属性,例如:
                静态属性: 都有重量、年龄等,你甚至可以给它取个名字来当做他的静态属性。
                动态属性: 都需要进食,都可以行动等。


如果将这些属性都写进猪狗鸡鸭这些类的话,那么上面的这些属性难免会重复写上几次,造成代码的冗余,那么我们就可以将这些共有的特性抽取出来抽象成一个类,然后让这些普通类包含这个类的属性,这就叫继承

        2、如何继承

        1、关键字extends 

代码如下:

class Animal{
    public String name;
    public int age;
    public void eat(){
        System.out.println(name+"正在吃饭");
    }
}
class Dog extends Animal{
    public void wangwang(){
        System.out.println(name+"正在汪汪叫");
    }
    public void eat(){
        System.out.println(name+"正在吃狗粮");
    }
}
class Bird extends Animal{
    public String wing;
    public void fly(){
        System.out.println(name + "正在飞");
    }
    @Override
    public void eat(){
        System.out.println(name+"正在吃鸟粮");
    }
}

         这个被共用的Animal类称为父类,这些共用父类属性的被称为子类,其基本的思想就是子类基于某个父类进行扩展,得到一个新的子类。让这个子类也可以继承父类所具有的属性,这个子类同时也可以自己增加父类所不具备的属性,

        子类对象无法调用父类中被private修饰的成员,如图:

 这里显示 index  在父类Animal中被 private修饰,无法访问从而报错。对于成员方法同样如此。

子类只能调用父类中被public 或者 protected 修饰的成员变量或方法。

在子类中可以使用super来调用父类的方法或者成员,也可以通过super来进行父类的构造。

        2、构造方法

如果提供了子类的构造方法,那么编译器默认不再提供不带参数的构造方法,如果自己提供了子类的构造方法,那么必须先要帮助父类进行构造:

class Animal{
    public String name;
    public int age;

    // 父类构造方法
    public Animal(String name, int age){
        this.name = name ;
        this.age = age ;
    }
}
class Dog extends Animal {
    public int dog_spec1;
    public int dog_spec2;


    // 子类构造方法
    public Dog(String name, int age, int dog_spec1, int dog_spec2) {
        super(name, age); // 在子类构造完成之前先帮助父类构造
    // 父类构造完成
        this.dog_spec1 = dog_spec1;
        this.dog_spec2 = dog_spec2;
    }
    // 子类构造完成
}

 在继承的处理机制当中,党实例化一个子类对象时,父类对象也相应的被实例化,换句话说,在实例化子类对象的时候,java编译器会在子类的构造方法中自动调用父类的无参构造方法或者提供的构造方法。

        3、重写

子类在继承父类后并不只是拥有了父类的属性,还可以对父类的属性进行拓展,即对父类的成员方法进行重写。

        重写就是在子类中保留父类中方法的方法名,然后重写方法体里面的实现内容,更改成员方法的权限,在上面关键字extends 的代码例子中,dog和bird两个类都继承了Animal类然后都重写了Animal类中的eat方法,这里不再举例。

        注意:当重写父类方法的时候,修改方法的权限只能从小范围到大范围的改变,例如,父类中的eat方法被public 修饰,那么子类重写的eat方法就不能被private和protected 修饰。

        

        3、Object类

        java中有一种比较特殊的类,Object类是所有类的父类,是java类中的最高层的类。在用户创建一个类的时候,除非这个类已经指定了要继承某一个类(java中一个类只能有一个父类),那么它就是默认java.lang,Object 类中继承Object类,(java中每一个类都源于java.lang.Object 类)。由于所有类,除了已经有父类的子类,都是Object的子类,所以在定义类的时候,extends Object 可以省略不写。

       

        1、Obejct类中方法:

①clone()方法

         创建并返回此对象的副本,一般表达式为x.clone!= x (意思是和原来的对象不是同一个对象)。想要克隆一个对象,首先这个对象的类要实现Cloneable接口,否则则会抛出CloneNotSupportedException异常。

        在java中创建对象有两种方法,一种是我们所熟悉的关键字new,一种是Object类里面的clone()方法。

        那么new是如何创建对象的过程是怎么样的? new关键字用于创建类的新实例对象。new首先会根据new的对象的类型来分配空间。分配完之后,再调用构造函数,为对象进行初始化,构造方法结束后,一个对象创建完毕,然后就会返回这个对象所在堆区的引用地址,在外面就可以使用这个类的引用来接收这个地址并对这个对象进行相关操作。 

         都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用源对象中对应的各个域,填充新对象的域, 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。

        而clone()方法,会在堆区生成一个和源对象类相同的对象,然后将源对象的内容填充到新的对象里,这个新对象存放在堆区的不同地方,然后返回这个新对象的引用地址,在外部可以使用所兼容的类的引用来接收这个地址,即x.clone!= x。

        如何使用clone()方法?

使用clone来复制一个对象的时候需要实现Cloneable这个接口,由于clone()这个方法源自Object类,我们在idea中按住左ctrl然后用左键点击clone进去可以发现里面是一个没有方法体的方法

( 由翻译得知,这段绿色的文字建议我们重写这个clone方法)

如果需要复制某个对象,成生一个新的副本,则在这个对象的类实现了这个Cloneable接口之后,还需要对这个clone方法进行重写,在idea中,在类中点击鼠标左键,然后选择Generate快速生成clone方法的重写,如图:

 throws为异常关键字,它用于方法体内部,并且抛出一个异常,党程序执行到throw语句时立即终止,他后面的语句将不会执行。如果不想他抛出异常,现阶段只用将throw 及后面的异常 复制粘贴到main方法的后面。

 我们使用clone来克隆一个对象,但是由于clone方法返回的是一个object类,因此此时相当于将一个父类赋值给一个子类,就形成了向下转型,对于克隆的返回值还需要进行一个强制类型转化,将其转化为与接收对象相同的类类型:

代码如下:

 以学生为例子,创建一个Student类,并实现Cloneable接口,然后重写clone方法。

class Student implements Cloneable{
    String name;
    int age;
    int score;

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

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

    @Override
    public String toString() {
        return "Student"+ "name:"+name +"  age: "+age + " score:"+score;
    }
}
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student("zhangsan",18,100);
        Student student2 = (Student) student1.clone();
        System.out.println(student1);
        System.out.println(student2);
    }
}

 打印结果如下:

 深浅克隆:

我们在上面的Student的类外面定义一个另外一个类Money(可以认为money是每个人的刚需必须有而生成的一个组合类型),我们暂时忽略student类当中的其他成员变量,只写入一个Money类来组合,代码如下

class Money{
    public int money = 10;
}
class Student implements Cloneable{

    public Money m = new Money();
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    @Override
    public String toString() {
        return "Student"+ " money:"+m;
    }
}
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student();
        Student student2 = (Student) student1.clone();
        System.out.println(student1.m.money);
        System.out.println(student2.m.money);
        student1.m.money = 100;
        System.out.println(student1.m.money);
        System.out.println(student2.m.money);
    }
}

解释如下:

我们给出的Money类中的有一个money成员,给出的默认值为10,然后将这个类和Student类进行组合,在mian方法中生成一个新的student1对象,并对其进行克隆,然后用一个student2接收这个引用地址,重写tostring方法后进行打印。后将student其中的Money类生成的m对象中的money改为100,然后再进行money的打印。结果如下:

 对比结果可以发现,修改student1的money的值后,student2 的money值也被修改了。
这是为什么呢???
这是因为clone在赋值对象的时候,其中的Money类的对象m的引用也复制了,
即:student1.m == student2.m   
修改student1 的m的对象的内容,就相当于修改了student2的m对象的内容;
为了印证这个猜想,我们对其hash值进行打印:结果发现相同

图解如下

这种克隆称为浅克隆,也叫作浅拷贝。

 如何进行深克隆???

如果要进行深克隆,就需要继续重写clone方法。针对克隆的m是相同引用的问题,我们做出修改:在克隆出一个student对象后,将里面的Money类的m对象的引用进行修改,要么使用new来新建一个m对象,或者使用clone对当前对象的Money类类型的m对象进行再次克隆。
注意:为了实现对m的拷贝,m的Money类也需要实现Cloneable接口并重写clone方法。

    protected Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        //student.m = new Money();
        //student.m.money = this.m.money
        student.m = (Money)m.clone(); // m的Money类型也需要接口和重写clone方法
        return  student;
    }

②equals方法

在java中我们经常使用 == 运算符来比较其左右两端的数据,其规则如下:

如果 == 两侧都是相同的基本数据类型,则比较其值是否相同,返回true或者fasle
如果 == 两侧是对象的引用,则比较其引用的地址是否相同,例如:
 

public class Test {

        public static void main(String[] args) throws CloneNotSupportedException {
            String str1 = new String("123");
            String str2 = new String("123");
            if ( str1 == str2 ){
                System.out.println("True");
            }else {
                System.out.println("False");
            }
            /
            String str3 = "123";
            String str4 = "123";
            if (str3 == str4){
                System.out.println("True");
            }else {
                System.out.println("False");
            }
            if (str1.equals(str2)){
                System.out.println("True");
            }else {
                System.out.println("False");
            }


        }
}

 结果为:

我们在main函数里面新创建两个str1和str2对象,然后用 == 对其进行比较,因为他们是两个不同对象的引用,所以第一个输出位false,而str3和str4,两个都是指向的同一个内容“123”,在str3去创建的时候,“123”这个字符串被放入了堆区的内存池中,str3指向这个字符串,当str4去创建一个字符串对象的时候,程序会首先去内存池查看是否已经存在相同内容的字符串,如果存在,就将其指向已经存在的字符串,否则就会生成一个新的字符串对象》》:

而.equals方法则是比较两个引用的对象的实际内容是否相同,案例如上图代码,因为str1和str2都的对象的内容都是:字符串"123",所以第三个值为true。
但是由于在自定义类中使用equals方法比较两个同类的不同对象的时候,equals方法的默认实现是使用“==”,运算符来比较两个对象的引用地址,而不是比较对象内容,所以要想真正做到比较两个自定义类的对象的内容,还需要在自定义类中重写equals方法。

③getClass()方法

他会返回对象执行时的Class实例,然后使用此实例调用getName()方法取得实例对应的类的名称,语法如下:

getClass().getName();

使用getClass方法返回并用Class对象来接收这个类的实例,并以此调用他的信息,例如这个实例里面的方法等。

④toString()方法

功能是将一个对象以字符串的形式输出,他会返回一个String实例,通常toString方法在自定义类中需要重写,来完整地输出这个对象的字符串形式。案例如下:

import java.lang.Object;
class Student{
    String name;
    int age;
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return name + " " + age;
    }
}
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student("zhangsan",18);
        System.out.println(student1);
    }
}

输出结果为

 
实例化一个student1对象,在重写toString方法后,调用打印函数对其进行输出,结果如上图。

但是为什么这个打印函数会自动调用toString方法???
我们在idea中按住ctrl+鼠标左键单机println,结果如下:

 发现接收类型使一个Object类的x,Object类是所有类的父类,这里发生向上转型,在synchronize里面打印s,于是我们再次按住ctrl加鼠标左键单机进入String s = 后面的valueOf查看Object类的x实例转化为String s 的原理,情况如下:

 发现,这里调用了你传进来的Object类的实例obj的方法toString (),而toString()方法已经在我们的子类中被重写,此时发生了动态绑定,jvm会依次在Student,Object类中查找(顺着继承链)实现了该toSting方法的类,并调用。此处已经在Student类中实现。

同时这也是多态的思想,关于多态,后面会讲到。

 二、多态

           对于几个不同的类,可以根据他们共同特性,将其抽取出来形成一个共同的父类,这个父类中包含了其他类所具有的共同属性,例如这里有一个狗类,一个猫类,和一个鸟类,他们都有眼睛,都有脚,还可以拥有自己的名字等等静态属性,动态属性,就比如都会吃,喝跑等。

        但是对于不同的动物来说,比如狗和鸟,他们的跑这个行为是不一样的,对于狗来说是用四条腿来跑,而对于鸟来说,只能用两个脚跳,或者用两个翅膀飞,这个行为和狗对比起来是完全不一样的,那么对于其继承的父类来说,不可能只用父类的方法,来描述两个不同的行为,这些行为都是他们的独自的特性

        这个时候就只需要对其父类的这个方法于子类中进行重写,而不用在每一个子类都实现一个方法,于是就避免了大量重复的代码的编写。同时只需要实例化一个继承这个父类的子类对象,即可调佣相应的方法。

class Animal{
    public void action(){
        System.out.println("正在跑");
    }
}
class Dog extends Animal{
    public void action(){
        System.out.println("正在用四条腿跑");
    }
}
class Bird extends Animal{
    public void fly(){
        System.out.println("正在飞");
    }
}

例如这里有一个Animal父类,其中一个Dog类和Bird类继承这个Animal类。父类中有action这个方法,但是对于鸟和狗来说,他们的行为是不一样的,比如狗是用四条腿跑,而鸟是用翅膀飞。

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Bird bird = new Bird();
        dog.action();
        bird.action();
    }
}

结果如下:

三、抽象类

        有时候我们在实例化对象的时候,难免会将一个父类不小心给实例化了,但是有的情况是不允许父类被实例化的。例如一个父类Animal动物类,他被他的子类Dog继承。我们可以将Dog实例化成一个具体的dog,但是你并不能将一个动物给具体抽象出来,一个动物直接抽象出来会是什么?怎么归类?但是我们又只需要实例化子类对象,而不是父类对象。这就是一大麻烦,为了避免这个问题,我们需要将这个父类设置为抽象类,来避免父类被实例化。

        在实际解决问题的时候,通常将父类设置为抽象类,然后对这个父类进行继承和多态处理,继承关系中,越是在上方的类越抽象,例如狗类继承哺乳类,哺乳类继承动物类等。

        抽象类的语法如下:

public abstract class Test类名{         

        abstract void function( );

}

        abstract为定义抽象类关键字,使用abstract关键字定义的类为抽象类,而使用这个关键字定义的方法被称为抽象方法,这个方法不能有方法体,方法本身没有任何意义,它的作用只存在于被子类重写后。一个类如果有抽象方法,那么他必须被定义为抽象类(如果声明一个抽象方法,就必须将含有这个抽象方法的类定义为抽象类),实际上抽象类除了被继承之外没有任何意义,抽象类同时也保证了类在被继承的时候的安全性。

        对于继承这个抽象类的子类来说,子类需要重写这个抽象类中的所有的抽象方法。

四、接口类

        举一个很简单的例子,一个父类中存在属性:名字,年龄行为:跑,游泳(两个都是抽象方法);

然后我们定一个狗类(假设不会游泳),和一个鱼类都继承这个父类,那么对于狗类和鱼类来说都需要实现跑和游泳这两抽象方法,那么问题来了,狗如何去实现游泳这个方法?那么鱼又如何去实现跑这个方法?鱼不可能会跑,狗不能游泳,或者说狗类不用重写和实现这个方法,那么这个抽象方法是不可能被实现的,但是需要继承这个父类的名字和年龄等属性,为了应对这种问题,接口的概念就出现了。

        接口就是为了应对这种父类不能完全将子类中的共同特性抽取出来的情况。

语法如下:

关键字:interface

interface Test(接口名){

        void  function();  // 接口内的方法,省略abstract关键字。

}

使用implements 来让类实现该接口

在接口中,方法必须被定义为publicabstract的形式其他修饰限定符不被java编译器认可,即使该方法没有被public修饰,也是默认public权限,并且接口中被定义的任何字段都是自动被staticfinal修饰的

实现某个接口的类,这个类就必须实现重写这个接口中的方法。

案例如下:

interface Fly{
    void fly();
}
interface Run{
    void run();
}
class Dog implements Run{
    @Override
    public void run() {
        System.out.println("dog is running");
    }
}
class Bird implements Fly{
    public void fly(){
        System.out.println( "bird is flying");
    }
}
public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Bird bird = new Bird();
        dog.run();
        bird.fly();
    }
}

        结果如图

 接口类型也可是实现向上转型
 

interface Run{
    void run();
}
class Dog implements Run{
    @Override
    public void run() {
        System.out.println("dog is running");
    }
}
public class Main {
    public static void main(String[] args) {
        Run run = new Dog();
        run.run();
    }
}

结果如下:

五、final关键字

重写和继承的功能很强大,但是必然也存在着不允许被重写或者继承的情况,这个时候就可以使用final修饰类或者方法,来防止某些不允许被重写和继承的情况。

1、final防止重写

        为了方法方法被重写,需要在方法声明的开始处将final指定为修饰符,被final修饰的方法不能被重修。

class A{
    final void method(){
        System.out.println("hello");
    }
}
class B extends A{
    void method(){
        System.out.println("hello java");
    }
}

报错如下:

2、使用final放置继承

 在声明类的时候可以在开始的位置加上final关键字,以防止其被其他类继承:

final class A{
    // 方法体
}
class B extends A{
    // 方法体
}

执行后显示

        如果一个类被abstract和final同时修饰,是非法的。如果这个类被abstract修饰,那么其中必定包含abstract方法,abstract方法必须被重写,但是被final修饰的类,其中的方法都是隐式的被final修饰,也就是说其中的所有方法都不能被重写,因此同时被final和abstract修饰是没有意义的,是不合法的。

3、final修饰的变量

final不仅可以修饰方法和类,还可以用来修饰成员变量,如果一个变量被final修饰,那么它的值在其生命周期内就不能改变。但是可以给他初始化。final修饰的变量初始化可以直接赋值,也可以使用代码块和构造方法来对其进行初始化,不管怎么样,必须对其选取这几种方法中的一个来初始化

初始化:

1、可以使用代码块

class A{
    final int a;
    {
        a = 10;
    }
}

如果这个变量同时被static修饰,那么可以使用static静态代码块来对其进行初始化:

class A{
    final static int a;
    static {
        a = 10;
    }
}

2、构造器初始化

class A{
    final  int a;
    public A(int a) {
        this.a = a;
    }
}

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值