java面向对象编程精讲

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、包

包(package)是组织类的一种方式,使用包的目的是保证类的唯一性:
大白话讲:你代码中有一个Person类,你同学也写了一个Person类,如果将来你们代码需要一起用就会出现同名类而报错。

1.1导入包中的类

比如我们现在要得到一个时间戳:

import java.util.Date;
public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        Date data=new Date();//通过import java.util.Date;实现
        //如果你不想导入java.util这个包中的Date类,你可以按如下写法实现相同的效果
        java.util.Date date=new java.util.Date();//效果和上面的一样,但写法比较麻烦
        System.out.println(data.getTime());
    }
}

ps:import:“引入”,指引入类中需要的类
package:“包”,指类所在的包

我们需要用到java类库里的一些代码时,我们需要用import进行相应的导入,需要注意的是,我们import是导入一个具体的类而不能是导入一个具体的包,比如上面的得到时间戳

import java.util.Date;//正确
import java.util;//错误

关于import.util.*

*是一个通配符,它会导入一个包里面所有的类,那这个时候就有问题了:util下面有很多的类,那你import.util. *就一下子全部导入了嘛?

答案是否定的:java处理的时候,需要哪个类才会拿哪个类,这里要区别C语言的include<>,C中导入某个头文件就会把那个头文件的所有内容全部拿过来,java相对更细化一点。

比如上面写的import java.util.Date;我们用import java.util.*;代替

import java.util.*;
public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        Date data=new Date();
        System.out.println(data.getTime());
    }
}

我们只用了util里面Date的类,那么*也只会导入Date的类,而不是一股脑全拉过来。

我们实际工作或学习中还是建议导入具体的类,那个import java.xxx. *;范围还是太大了,有时候你把握不住。 举个栗子:

import java.util.*;
import java.sql.*;
public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        Date data=new Date();
    }
}

你可能当时是想用util里面的Data,但是后面有些程序又需要用sql里面的一些类,你一股脑util和sql都是用*来导入,没有导入具体的类,问题就出现了。

util和sql里面都有Data这个类,那电脑怎么知道你要导入哪个包里面的Date啊,这里就会报错。

1.2静态导入

静态导入,工作学习都使用的很少,大家了解即可。
使用import static可以导入包中的静态方法和字段。

举例说明:我们平时经常会用到System.out.println();System是一个类,我们通过import static java.lang.System.*;导入这个类,然后这个类底下的静态方法就可以直接使用了,我们直接out.println()即可完成打印

import static java.lang.System.*;
public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        out.println("hello");
    }
}

静态导入写出来的程序对理解程度要求高,且看起来稀奇古怪,我们了解即可,不需要深入理解。

1.3将类放到包中

基本规则:
1.在文件的最上方加一个package语句,指定该代码在哪个包中。
2.包名需要尽量指定成唯一的名字,通常会用公司的域名的颠倒形式(比如:你原先域名为www.bidu.com那么颠倒形式就是com.bidu.www)
3.包名要和代码路径相匹配,例如创建com.bit.demo1的包,那么会存在一个对应的路径com/bit/demo1来存储代码
4.如果一个类没有package语句,则将该类放到一个默认包中。

步骤如下:
1.在IDEA中先新建一个包,右键src->new>package
在这里插入图片描述
2.在弹出的对话框中输入包名,比如com.bit.demo1
在这里插入图片描述
3.在包中创建类,右键包名->新建->类,然后输入类名即可
在这里插入图片描述
4.可以看到我们的磁盘上的目录结构已经被IDEA自动创建了
在这里插入图片描述
5.同时我们也可以看到,在新建的test.java文件上方,出现了package语句
在这里插入图片描述

1.4包的访问权限控制

我们已经了解了类中的public和private。private里的成员只能被类的内部使用。如果,某个成员不含public和private关键字,那么这个成员可以在包内部的其他雷使用,但是不能在包外部的类使用
示例如下:
我们在一个包com.bit.demo1中创建了2个类:Demo1和Demo2
在这里插入图片描述
在Demo1中不加任何访问修饰符创建int val=1;
在这里插入图片描述
那么相同包下面的Demo2也可以用这个val
在这里插入图片描述
打印结果为1:
在这里插入图片描述
但是出了这个包,我们再来测试这段相同的代码
在这里插入图片描述
这里就会报错。

1.5常见的系统包

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

二、继承

2.1背景

代码中创建的类,主要是为了抽象现实中的一些事物(包括属性和方法)
有时候客观事物之间就存在一些关联关系,那么在表示成类和对象的时候,也会存在一定的关联。
代码示例如下:

class Dog{
    
    
    public String name;
    public int age;
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
}
class Bird{
    
    
    public String name;
    public int age;
    public String wing;//翅膀
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
    public void fly(){
    
    
        System.out.println(name+"正在飞行");
    }
}

我们现在写了两个类:Dog和Bird,它们作为动物有一些相同的属性和方法,比如name,age,eat(),那么我们可以把这些共性都抽象出来,放在一个Animal类里面 ,然后我们让Dog和Bird分别继承Animal类,即可达到代码重用的效果。

class Animal{
    
    
    public String name;
    public int age;
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
}

Animal这样被继承的类,我们称为父类、基类、或超类,对于像Dog和Bird这样的类,我们称为子类、派生类,也很好理解嘛,你父亲买的电脑,你也可以用它来打电动嘛哈哈哈。

2.2语法规则

class 子类 extends 父类{
    
    
}

1.使用extends指定父类
2.java中一个子类只能继承一个父类(C++或其他一些语言支持多继承)
3.子类会继承父类的所有public的字段和方法
4.对于父类的private的字段和方法,子类中是无法访问的
5.子类的实例中,也包含着父类的实例,可以用super关键字得到父类实例的引用
举例说明:(我们仍用上面的Dog和Bird举例)

class Animal{
    
    
    public String name;
    public int age;
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
}
class Dog extends Animal{
    
    
//原先相同的代码就可以不用写了
}
class Bird extends Animal{
    
    
    public String wing;//翅膀
    public void fly(){
    
    
        System.out.println("正在飞行");
    }
}
public class Demo1{
    
    
    public static void main(String[] args) {
    
    
        Dog d=new Dog();
        d.name="xiaoHei";
        System.out.println(d.name);
        d.eat();
    }
}

运行效果如下:
在这里插入图片描述
我们用子类创建一个对象后,它的在内存里是这样的:
在这里插入图片描述

我们子类继承父类后,我们new一个子类的对象,会在堆上创建父类有的东西和子类有的东西。比如我们上面的代码,狗是动物的子类,狗这个类里面本没有东西,但继承了动物里的name,age,那么堆里面就会开辟一块内存放name和age。Bird这个类里面有wing,再继承动物这个类里面的name和age,那么堆里面就会开辟一块空间放name、age、wing

当我们在父类中加入构造方法,原先的代码就会报错了,为什么呢?
在这里插入图片描述

子类构造的同时,需要先帮助父类进行构造,也就是子类里面要调用父类的构造方法,这里我是这样理解的:子类要继承一些东西,你父类得有那些东西,就像是你想继承你爸爸的餐厅啥的,你爸爸得有餐厅。而优先帮父类构造也就是我们下面要说的super

class Animal{
    
    
    public String name;
    public int age;
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
    public Animal(String name,int age){
    
    
            this.name=name;
            this.age=age;
    }
}
class Dog extends Animal{
    
    
    public Dog(String name,int age){
    
    
    super(name,age);
    //super:显示调用构造方法
    // 这里的super(name,age)就是调用父类的两个参数构造方法
}
}
class Bird extends Animal{
    
    
    public Bird(String name,int age,String wing){
    
    
    //这里传参个数不受父类构造方法影响,你需要几个参数你就传几个参数
        super(name,age);
        this.wing=wing;
    }
    public String wing;//翅膀
    public void fly(){
    
    
        System.out.println("正在飞行");
    }
}

我们在Dog和Bird里面都弄一个构造方法来调用父类的构造方法,
super(实际参数列表…)表示显示调用父类中的构造方法,它用来表示当前对象的父类型特征,就比如这里Dog这个类里面的super(name,age)就是表示狗子的名字和年龄,名字和年龄是父类动物的特征。

这个时候可能会有同学会问,那我们之前没有构造方法的时候为什么没有报错?在笔者上一篇java类和对象文章中,我们说过,一个类没有任何构造方法时,系统会自动生成一个无参的构造方法
那么我们这里子类父类会这样无参生成构造方法:

class Animal{
    
    
    public String name;
    public int age;
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
    public Animal(){
    
    
    }
}
class Dog extends Animal{
    
    
    public Dog(){
    
    
    super();
}

所以在你改变父类的构造方法后,子类原先和父类对应的无参构造就被破坏了,你也必须在子类中的构造方法做出相应的改变。
ps1:super只能在当前构造方法中使用
在这里插入图片描述
ps2:super在构造方法中必须处于第一行的位置,也侧面说明了,子类构造时必须先帮助父类先构造(super先进去构造父类,构造完再出来构造子类)
在这里插入图片描述
在这里插入图片描述
super的三种用法:
1.super();调用父类的构造方法,只能在构造方法中使用
2.super.func();调用父类的普通方法
3.super.data;调用父类的成员属性

用法1必须在构造方法中使用,2和3出现在哪里都无所谓,这些出现位置的用法和this非常相似,我们回顾一下this的用法:

1.this.data表示调用当前对象的属性
2.this.func()表示调用当前对象的方法
3.this()调用当前对象的其他构造方法——this()只能存在于构造方法中

注意!super不能出现在静态方法中,super是代表父类对象的引用,它是依赖于对象的,static修饰的静态方法是不依赖对象的,所以是不能共存的。this也一样,this是当前对象的引用,它也是依赖对象的,所以也不能放在静态方法中

重名

class Animal{
    
    
    public String name="abc";
    public int age;
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
    public Animal(String name,int age){
    
    
            this.name=name;
            this.age=age;
    }
}
class Dog extends Animal{
    
    
    public String name;
    public Dog(String name,int age){
    
    
    super(name,age);
    }
}
public class Demo1 {
    
    
    public static void main(String[] args) {
    
    
        Dog d=new Dog("xiaoHei",1);
        System.out.println(d.name);
    }

}

我们现在new了一个Dog类的d对象,传参过去xiaoHei和1(name参数和age参数),然后进入Dog类里面的构造方法,构造方法里的super把xiaoHei和1再传给父类,将父类里的name和age初始化了,现在问题是,我子类里面也有一个name,那我程序也没有报错啊,最后运行结果的name是谁呢?

我们用子类创建一个对象后,它的在内存里是这样的:
在这里插入图片描述
那同样的,我们现在如果Dog这个子类里面也有name这个属性,堆里面应该是有两个name,如果是子类应该是null,如果是父类应该是xiaoHei。
在这里插入图片描述

在这里插入图片描述
我们的运行结果是null,这里也就是涉及一个知识点:如果子类和父类有同名的字段了,那么是优先子类的字段。如果子类中没有父类的字段,用父类的

说个大白话:你爸爸买了一块蛋糕,你也买了一块蛋糕,你肯定优先吃你自己的蛋糕;你爸爸买了一块蛋糕,你没买蛋糕,你可以吃你爸爸的蛋糕。

那这个时候肯定也有一些小伙伴要问,那我如果子类和父类里面有同名字段,但我就是想用父类里面的东西,怎么办?
也有办法!就是我们之前说的super.name,不然默认还是优先子类

2.3protected关键字

我们在Animal里面多加一个count属性,并用private修饰

class Animal{
    
    
    public String name;
    public int age;
    private int count;
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
    public Animal(String name,int age){
    
    
            this.name=name;
            this.age=age;
    }
}
class Dog extends Animal{
    
    
    public String name;
    public Dog(String name,int age){
    
    
    super(name,age);
    //super:显示调用构造方法
    // 这里的super(name,age)就是调用父类的两个参数构造方法
    }
}
public class Demo1 {
    
    
    int val=1;//不加任何访问修饰符,默认是包访问权限
    public static void main(String[] args) {
    
    
        Dog d=new Dog("xiaoHei",1);
        System.out.println(d.name);
        System.out.println(d.count);//这里会报错
    }
}

在这里插入图片描述

会发现,我们在调用count这个对象的时候,会报错,是因为private只能在当前类使用,出了当前类就不能直接调用private修饰的东西,那我们把private改成protect试一试:

class Dog extends Animal{
    
    
    public String name;
    public Dog(String name,int age){
    
    
    super(name,age);
    //super:显示调用构造方法
    // 这里的super(name,age)就是调用父类的两个参数构造方法
    }
}
public class Demo1 {
    
    
    int val=1;//不加任何访问修饰符,默认是包访问权限
    public static void main(String[] args) {
    
    
        Dog d=new Dog("xiaoHei",1);
        System.out.println(d.name);
        System.out.println(d.count);//这里会报错
    }
}

你会发现系统不再报错,为什么?我们先来看一张图
在这里插入图片描述
(图片来自比特就业课)
public修饰的:什么地方都能用
private修饰的:只有当前类可以用
default修饰的:同一包中都可以用,出了这个包就不能用了
protected修饰的:同一包都可以用,或者出了包,但是是子类也可以用。

我们如果用public或者private修饰一个类,有时就过于激进了,protected好就好在,它对于子类继承有一个判断——继承了可以访问,不继承不能访问

2.4更复杂的继承关系

刚才我们的例子中,只涉及到了Animal、Dog、Bird三种类,那如果情况更复杂呢?我们Animal下面还有猫,猫下面还有更多品种的猫:
在这里插入图片描述
(图片来自比特就业课)
这时候使用继承方式来表示,会涉及到更复杂的体系。
在这里插入图片描述

2.5final关键字

如果一个类不想被继承,我们可以设置为final,很好理解嘛:你不想生孩子继承你的花呗,那你就是你家族最后的final的一个。(哈哈哈开个玩笑)
ps:final的一些其他用法
final int a=10;//常量不可被修改
final class A //代表整个类不可以被继承
final修饰方法,后面再讲解

三、组合

和继承类似,组合也是一种表达类之间关系的方式,也能够达到代码重用的效果。组合是a part of…的意思,比如一个学校由学生和老师组成,那么学生是学校的一部分,老师也是学校的一部分。
在这里插入图片描述
(图片来自比特就业课)
我们组合不需要像继承那样写extends这样的关键字,仅仅是将一个类的实例作为另一类的字段(如上图)。这是我们设计类的一种常用方法之一。

四、多态

4.1向上转型(子类对象给父类)

刚才的例子中,我们可能会写形如下面的代码:

Bird bird=new Bird("肥波");
//我们这里假设构造方法里只接受name这一个参数

这个代码也可以写成如下形式

Bird bird=new Bird("肥波");//bird是一个引用
Animal bird2=bird;//bird2也是一个引用,
//这段代码表示bird2指向bird所指的对象
//或者
Animal bird2=new Bird("肥波");
//父类引用 来引用 子类对象

此时bird2是一个父类(Animal)的引用,指向一个子类Bird的实例,这种写法称为向上转型

什么时候会发生向上转型呢?
1.直接赋值:

Animal bird2=new Bird("肥波");

2.方法传参

public static void func(Animal animal){
    
    

    }
public static void main(String[] args) {
    
    
        Dog dog=new Dog("xiaoHei",1);
        func(dog);
    }
//dog是子类创建的对象,然后你把它传过去给父类创建的引用指向

3.作为返回值

 public static Animal func2(){
    
    
        Dog dog=new Dog("xiaoH",1);
        return dog;
    }

你在func2这个方法里new了一个dog对象,然后返回的时候把这个对象交给Animal这个类的引用来指向(你返回的时候肯定有东西接收嘛,接收那头肯定是Animal 引用名=返回值,还是相当于父类引用 引用了 子类对象

这种向上转型我们可以这样理解:
你让你女朋友去喂肥波,你可以说:“媳妇你喂小鸟了吗”或者“媳妇你喂肥波了吗”

4.2动态绑定

我们先来看一段代码:

class Animal{
    
    
    public String name;
    public int age;
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
    public Animal(String name,int age){
    
    
            this.name=name;
            this.age=age;
    }
}
class Dog extends Animal{
    
    
    public Dog(String name,int age){
    
    
    super(name,age);
    }
}
public class Demo1 {
    
    
    public static void main(String[] args) {
    
    
        Dog dog=new Dog("xiaoHei",1);
        dog.eat();
    }
}

在这里插入图片描述
这里我们new了一个狗子,然后传参过去Dog类,由super传到Animal类。然后我们由dog.eat()调用eat方法,打印了xiaoHei正在吃东西。

但有时候,我们的子类和父类会出现同名的方法,那我们再去调用会发生甚么事呢?我们在原先代码上稍作修改:我们给狗子这个类多写一个eat方法区别于其他动物的eat。

class Animal{
    
    
    public String name;
    public int age;
    public void eat(){
    
    
        System.out.println(name+"正在吃东西");
    }
    public Animal(String name,int age){
    
    
            this.name=name;
            this.age=age;
    }
}
class Dog extends Animal{
    
    
    public String name;
    public Dog(String name,int age){
    
    
    super(name,age);
    }
    public void eat(){
    
    
        System.out.println(name+"正在吃骨头");
    }
}
public class Demo1 {
    
    
    public static void main(String[] args) {
    
    
        Dog dog=new Dog("xiaoHei",1);
        dog.eat();
    }
}

我们new了一个dog,然后传参过去Dog的类,再由super传参给Animal,如果我们Dog类里面没有eat方法,我们肯定会打印,xiaoHei正在吃东西,但是当子类里有和父类同名的eat会怎样呢?运行结果如下:
在这里插入图片描述
在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.(只有父类的方法用父类的,子类和父类都有同名的用子类的)

动态绑定:
1.父类引用,引用 子类的对象
2.通过这个父类引用 调用父类 和子类同名的覆盖方法

ps:同名的覆盖也就是下面要讲的重写(方法返回值相同,方法名相同,参数列表相同,内容不一定相同)

4.3方法重写

针对刚才的eat()方法来说:子类实现父类的同名方法,并且参数的类型和个数完全相同,这种情况称为覆盖、重写、覆写
重写要满足4个条件:
1.方法名相同
2.参数列表相同
3.返回值相同
4.父子类的情况下
一般重写的方法前我们加一个@Override

class Dog extends Animal{
    
    
    public String name;
    public Dog(String name,int age){
    
    
    super(name,age);
    }
    @Override
    public void eat(){
    
    
        System.out.println(name+"正在吃骨头");
    }
}

@Override就是告诉别人,这个方法是重写的
重写的一些注意事项:
1.静态方法不能重写
2.子类的访问修饰限定符范围应大于等于父类访问修饰限定符
(比如子类是public,父类是protected)
3.private方法不允许重写
4.final修饰的方法也不允许重写

ps:重写和重载的区别:
在这里插入图片描述
(图片来自比特就业课)
重载的这种方式有时也称静态绑定,系统会根据你方法的参数来确定你要走哪个方法(重载的方法参数不一定相同),区别重写的动态绑定(函数名、参数、返回值必须相同)。

4.4理解多态

知道向上转型、动态绑定、方法重写后,我们就可以使用多态的形式来设计程序了。比如:我们可以写一些只关注父类的代码,就可以同时兼容各种子类的情况。
代码示例如下:

class shape{
    
    
    public void draw(){
    
    
        System.out.println("正在画画");
    }
}
class Cycle extends shape{
    
    
    @Override
    public void draw(){
    
    //重写快捷按键:dr
        System.out.println("正在画圆");
    }
}
class Flower extends shape{
    
    
    @Override
    public void draw(){
    
    
        System.out.println("正在画花");
    }
}

public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        shape s=new shape();
        s.draw();
        s=new Flower();
        s.draw();
        s=new Cycle();
        s.draw();
    }
}

运行结果如下:
到这里,我们就更深层次的理解多态了——通过一个引用调用同一个方法,会有不同的表现形式(取决于它引用哪个对象)
在这里插入图片描述
ps:1.多态的大前提——一定是向上转型
使用多态的好处是什么?

  1. 类调用者对类的使用成本进一步降低.
    封装是让类的调用者不需要知道类的实现细节.
    多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
    因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
  2. 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
    比如我现在要打印如下的形状
    ①不用多态,如果子类比较少还好,但如果子类比较多else if就会非常麻烦
public static void drawShapes() {
    
     
 Rect rect = new Rect(); 
 Cycle cycle = new Cycle(); 
 Flower flower = new Flower(); 
 String[] shapes = {
    
    "cycle", "rect", "cycle", "rect", "flower"}; 
 
 for (String shape : shapes) {
    
     
 if (shape.equals("cycle")) {
    
     
 cycle.draw(); 
 } else if (shape.equals("rect")) {
    
     
 rect.draw(); 
 } else if (shape.equals("flower")) {
    
     
 flower.draw(); 
 } 
 } 
} 

②用多态,这样的代码不仅简单而且高大上,容易让你的同学老师直呼:“牛逼哇塞!”

public static void drawShapes() {
    
     
 // 我们创建了一个 Shape 对象的数组. 
 Shape[] shapes = {
    
    new Cycle(), new Rect(), new Cycle(), 
 new Rect(), new Flower()}; 
 for (Shape shape : shapes) {
    
     //for each,每次循环定义一个shape来接收数组shapes里的内容
 shape.draw(); 
 } 
} 

4.5向下转型(父类对象给子类)

public static void main(String[] args) {
    
    
        Animal animal=new Dog("xiaoHei",1);
        //new Dog()给animal 相当于狗是动物
        Dog dog=(Dog)animal; //将父类引用强转给子类引用
       //相当于动物是狗(大多数情况有问题)
        dog.eat();
    }

但我们一般不建议这么写,就拿这个代码来说,很简单的道理:不是所有animal都是dog,这样很容易出错的!所有我们向下转型有一个大前提:就是你父类引用的对象要和你父类向下转型的对象匹配上,还是拿这块代码说:你animal接收了一个dog,那你animal后面向下转型只能给dog,不能给bird等等其他

public static void main(String[] args) {
    
    
        Animal animal=new Dog("xiaoHei",1);
        Bird bird=(Bird)animal; 
        bird.fly();
    }

比如上面这块代码,我们new了一个狗赋给动物(相当于狗是动物),然后你又把动物赋给鸟,前后联系起来就相当于狗是鸟,这明显逻辑不通。代码运行时也确实会报错:类型转换异常。

为了让向下转型更加安全,我们有了instanceof,instanceof可以判断一个引用是否是某个类的实例,如果是,则返回true,这时再进行向下转型操作就比较安全了。

Animal animal=new Cat("小猫");
if(animal instanceof Bird){
    
    
Bird bird=(Bird)animal;
}

4.6super关键字

前面的代码中,由于使用了重写机制,调用到的是子类的方法,如果需要在子类内部调用父类方法怎么办?可以使用super关键字。
super表示获取到父类实例的引用,涉及两种常见用法:
1.使用super来调用父类的构造器(前面已经讲过)

public Bird(String name){
    
    
    super(name);
}

2.使用super来调用父类的普通方法

public class Bird extends Animal {
    
     
 public Bird(String name) {
    
     
 super(name); 
 } 
 @Override 
 public void eat(String food) {
    
     
 // 修改代码, 让子调用父类的接口. 
 super.eat(food); 
 System.out.println("我是一只小鸟"); 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 

在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了). 而加上 super 关键字, 才是调用父类的方法

super和this有一些相似,但其中也有区别:
在这里插入图片描述
(图片来自比特就业课)

4.7在构造方法中调用重写的方法

我们先来看一段坑人的代码:

class B {
    
     
 public B() {
    
     
 // do nothing 
 func(); 
 } 
 public void func() {
    
     
 System.out.println("B.func()"); 
 } 
 
} 
class D extends B {
    
     
 private int num = 1; 
 @Override 
 public void func() {
    
     
 System.out.println("D.func() " + num); 
 } 
 
} 
public class Test {
    
     
  public static void main(String[] args) {
    
     
     D d = new D(); 
     }
}

这里我们new了一个D类对象,然后进去D,因为D中默认会有一个super(),这里没有写,但是我们要知道super是存在的,然后super进去父类B构造方法,B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func,此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0。

五、抽象类

在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class).

5.1语法规则

abstract class Shape {
    
     //包含抽象方法的类也必须用abstract修饰
 public abstract void draw(); //抽象类中**必须**有一个抽象方法!!!,抽象方法要用abstract修饰
 public int a;//抽象类中可以有成员变量
 public void func(){
    
    //可以有成员方法
        System.out.println("一个普通方法");
    } 
}
class Rect extends shape{
    
    
    @Override//子类如果继承抽象类,必须重写抽象类所有的抽象方法
    public void draw() {
    
    
        System.out.println("正在画方块");
        System.out.println(a);//子类继承抽象类可以访问抽象类里面的成员
        super.func();//也可以通过super来调用抽象类里面的普通方法
    }
}
class Cycle extends shape{
    
    
    @Override
    public void draw(){
    
    
    //重写快捷按键:因为我们这里是draw方法,你按一个dr就显示出来了,其他方法以此类推
    //比如func()方法,你重写的时候,按一下fu电脑也可快速进行重写
        System.out.println("正在画圆");
    }
}
class Flower extends shape{
    
    
    @Override
    public void draw(){
    
    
        System.out.println("正在画花");
    }
}

注意事项

  1. 抽象类不能直接实例化
shape shape = new Shape(); 
// 编译出错
Error:(30, 23) java: Shape是抽象的; 无法实例化
  1. 抽象方法不能是 private 的
    这个很好理解,抽象类要被其他子类继承的啊,你老爹把东西都藏起来了,你继承个啥啊
abstract class Shape {
    
     
 abstract private void draw(); 
} 
// 编译出错
Error:(4, 27) java: 非法的修饰符组合: abstractprivate
  1. 抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用
abstract class Shape {
    
     //包含抽象方法的类也必须用abstract修饰
 public abstract void draw(); //抽象类中**必须**有一个抽象方法!!!,抽象方法要用abstract修饰
 public int a;//抽象类中可以有成员变量
 public void func(){
    
    //可以有成员方法
        System.out.println("一个普通方法");
    } 
}
class Rect extends shape{
    
    
    @Override//子类如果继承抽象类,必须重写抽象类所有的抽象方法
    public void draw() {
    
    
        System.out.println("正在画方块");
        System.out.println(a);//子类继承抽象类可以访问抽象类里面的成员
        super.func();//也可以通过super来调用抽象类里面的普通方法
    }
}

一些特殊的注意点

1.如果一个抽象类继承了另一个抽象类,那这个抽象类可以不重写父类的抽象方法

abstract class shape{
    
    
    public int a;
    public void func(){
    
    
        System.out.println("一个普通方法");
    }
    public abstract void draw();
    /*{
        //System.out.println("正在画画");
    }*/
}
abstract class A extends shape{
    
    
    public abstract void func2();
}

2.但如果继承抽象类的抽象类再次被继承(比如上面的类A被继承了),你就必须把之前欠的抽象方法全部写上

abstract class shape{
    
    
    public int a;
    public void func(){
    
    
        System.out.println("一个普通方法");
    }
    public abstract void draw();
    /*{
        //System.out.println("正在画画");
    }*/
}
abstract class A extends shape{
    
    
    public abstract void func2();
}
class B extends A{
    
    
    @Override
    public void draw() {
    
    //"爷爷的抽象方法"
        
    }
    @Override
    public void func2() {
    
    //"爸爸的抽象方法"

    }
}

大家可以按我这个理解方式来进行简单的记忆:抽象方法相当于你开的花呗,你爸爸不还,你来还,你仍然不还,你儿子来还。。。出来混迟早要还的哈哈哈

5.2抽象类的作用

抽象类存在的最大意义就是为了被继承.
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法
这个时候可能会有人说:普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?确实如此. 但是使用抽象类相当于多了一重编译器的校验

使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.
小结:
在这里插入图片描述

六、接口

接口是抽象类的更进一步(你可以理解它是一个特殊的抽象类). 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含静态常量.

6.1语法规则

在这里插入图片描述
在这里插入图片描述

代码如下:

interface IShape{
    
    //接口用interface修饰
    //接口的所有方法都用public
    //抽象方法默认public abstract
    public abstract void draw();//抽象方法
    //直接写void draw()也可以,因为默认是public abstract
    
    default public void func(){
    
    
        //接口中普通方法不能有具体实现,如果一定要实现,在方法前加default
        //default是默认的意思,也就是该方法是接口默认方法
        System.out.println("第一个默认方法");
    }
    default public void func2(){
    
    //接口可以有n多个默认方法
        System.out.println("第二个默认方法");
    }
   
    public static void staticFunc(){
    
    //接口可以有多个静态方法
        System.out.println("静态方法");
    }
   
    public static final int a=1;//接口中也可以包含字段,但必须是静态常量(final static),
    //这里必须定义a后必须初始化(你是最终值,那你得有个最终值啊)
    //这里你直接写 int a=1;也可,因为是默认public static final的
}

class Triangle implements IShape{
    
    //类和接口之间的关系是通过implements实现的
    //一个类实现了一个接口必须重写抽象方法(接口具有抽象类的性质:必须重写抽象类的抽象方法)
    // 默认方法/静态方法你愿意重写就重写,不愿意也可以不写
    @Override
    public void draw() {
    
    
        System.out.println("画一个三角");
    }
 
    @Override//默认方法可重写,可不重写
    public void func(){
    
    
        System.out.println("重写接口中默认方法");
    }
}

public class TestDemo {
    
    
    public static void main(String[] args) {
    
    
        IShape iShape=new Triangle();
        iShape.draw();
        iShape.func();
    }
}

运行结果如下:
在这里插入图片描述
一些注意点:
1.使用interface定义一个接口

2.接口中的方法一定是抽象方法,因此可以省略abstract

3.接口中的方法一定是public,因此可以省略public

4.我们代码中的Triangle使用implements继承接口,此时表达的含义不再是“扩展”而是“实现”

5.在调用的时候同样可以创建一个接口的引用,对应到一个子类的实例(向上转型,比如上述代码中的 IShape iShape=new Triangle();)

6.接口不能单独被实例化(你不能new一个接口)

扩展(extends):当前已有一些功能,进一步扩充功能
实现(implements):当前什么也没有,需从头构造

6.2实现多个接口

如下面代码中的c,如果想同时实现Ia和Ib两个接口,可以通过implements来实现,多个接口由逗号隔开。比如class c implements Ia,Ib 这里需要注意的是,实现多个接口,那你也必须需要重写每一个接口的抽象方法

interface Ia{
    
    
    void funcA();//默认为public abstract
}
interface Ib{
    
    
    void funcB();
}
class c implements Ia,Ib{
    
    
    @Override
    public void funcA() {
    
    
        
    }

    @Override
    public void funcB() {
    
    
        
    }
}

这里需要注意的是,抽象方法的顺序必须对应多个接口的顺序,比如Ia的必须对应funcA,Ib的必须对应funcB。

举例说明
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.
然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.现在我们通过类来表示一组动物

class Animal {
    
     
 protected String name; 
 
 public Animal(String name) {
    
     
 this.name = name; 
 } 
} 

另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”.

//不是所有动物都会飞、游泳等,我们不能直接把这些特性写到Animal类中
//一个类不能同时继承多个类,但可以实现多个接口
interface IFlying {
    
     
 void fly(); 
} 
interface IRunning {
    
     
 void run(); 
} 
interface ISwimming {
    
     
 void swim(); 
} 

接下来我们创建几个具体的动物
猫, 是会跑的.

class Cat extends Animal implements IRunning {
    
     
 public Cat(String name) {
    
     
 super(name); 
 } 
 @Override 
 public void run() {
    
     
 System.out.println(this.name + "正在用四条腿跑"); 
 } 
} 

鱼, 是会游的.

class Fish extends Animal implements ISwimming {
    
     
 public Fish(String name) {
    
     
 super(name); 
 } 
 @Override 
 public void swim() {
    
     
 System.out.println(this.name + "正在用尾巴游泳"); 
 } 
} 

还有一种神奇的动物, 水陆空三栖, 叫做 鸭子

class Duck extends Animal implements IRunning, ISwimming, IFlying {
    
     
 public Duck(String name) {
    
     
 super(name); 
 } 
 @Override 
 public void fly() {
    
     
 System.out.println(this.name + "正在用翅膀飞"); 
 } 
 @Override 
 public void run() {
    
     
 System.out.println(this.name + "正在用两条腿跑"); 
 } 
 @Override 
 public void swim() {
    
     
 System.out.println(this.name + "正在漂在水上"); 
 } 
} 

上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 .

猫是一种动物, 具有会跑的特性.
青蛙也是一种动物, 既能跑, 也能游泳
鸭子也是一种动物, 既能跑, 也能游, 还能飞

几组测试案例:

public class test {
    
    
    public static void running(IRunning iRunning) {
    
    
        iRunning.run();
    }

    public static void swimming(ISwimming iSwimming) {
    
    
        iSwimming.swim();
    }
    public static void flying(IFlying iFlying){
    
    
        iFlying.fly();
    }

    public static void main(String[] args) {
    
    
        running(new Duck("鸭子"));
        //如果能成功传参一个说明这个对象具有接口里描述的能力
        //(对象的类已经实现了接口)
        flying(new Duck("鸭子"));
        swimming(new Fish("鱼"));
    }
}

运行结果如下:
在这里插入图片描述

这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力.

6.3接口的继承

接口和接口之间,可以使用extends来操作它们的关系(意为拓展),一个接口IB通过extends来拓展另一个接口IA的功能。当一个类Ic通过implements来实现接口IB时,重写的方法不仅仅是IB的抽象方法,还有IB从C接口拓展来的抽象方法funcA
代码示例如下:

interface IA{
    
    
    void funcA();
}
interface IB extends IA{
    
    //接口IB通过继承,也拥有了接口IA的功能
    void funcB();
}
class Ic implements IB{
    
    
    @Override
    public void funcB() {
    
    

    }//只重写一个funcB会报错,因为你IB也继承了IA

    @Override
    public void funcA() {
    
    
        
    }
}

6.4Clonable接口和深拷贝

Java 中内置了一些很有用的接口, Clonable 就是其中之一

Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常.

class Animal implements Cloneable {
    
    
        private String name;
        @Override
        public Animal clone() {
    
    
            Animal o = null;
            try {
    
    
                o = (Animal)super.clone();
            } catch (CloneNotSupportedException e) {
    
    
                e.printStackTrace();
            }
            return o;
        }
    }
    public class Test {
    
    
        public static void main(String[] args) {
    
    
            Animal animal = new Animal();
            Animal animal2 = animal.clone();
            System.out.println(animal == animal2);
        }
    }
// 输出结果
// false 

浅拷贝 VS 深拷贝
Cloneable 拷贝出的对象是一份 “浅拷贝”
观察以下代码:

public class Test {
    
    
        static class A implements Cloneable {
    
    
            public int num = 0;
            @Override
            public A clone() throws CloneNotSupportedException {
    
    
                return (A)super.clone();
            }
        }
        static class B implements Cloneable {
    
    
            public A a = new A();
            @Override
            public B clone() throws CloneNotSupportedException {
    
    
                return (B)super.clone();
            }
        }
        public static void main(String[] args) throws CloneNotSupportedException {
    
    
            B b = new B();
            B b2 = b.clone();
            b.a.num = 10;
            System.out.println(b2.a.num);
        }
    }
// 执行结果
//10 

通过 clone 拷贝出的 b 对象只是拷贝了 b 自身, 而没有拷贝内部包含的 a 对象. 此时 b 和 b2 中包含的 a 引用仍然是指向同一个对象. 此时修改一边, 另一边也会发生改变.
未来学到序列化的时候, 会告诉大家如何进行深拷贝.

6.6小结

抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别(非常重要!! ).
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法.
如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此,此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口

class Animal {
    
     
 protected String name; 
 
 public Animal(String name) {
    
     
 this.name = name; 
 } 
} 

猜你喜欢

转载自blog.csdn.net/m0_57180439/article/details/121316831