Java中面向对象的三大特性 -- 有关多态

学习目标

  1. 理解多态
  2. 掌握instanceof
  3. 了解抽象类,抽象方法

1.多态(向上转型)

● 现在我们已经学会了继承(类与类之间的)关系,并且能够在子类继承父类的基础上进一步对子类的数据及操作进行扩展,增加新的成员变量和方法或者对父类的方法进行重写覆盖。
● 不过重写了父类方法之后,我们仍然按照传统的方法构建子类对象后对方法进行调用,看起来继承仅仅是帮助我们省去了一部分变量、方法声明的代码编写而没有其他特殊的作用。
● 但是如果只有这一点好处,继承也不能被称为是面向对象编程语言的一大基石了,事实上他还作为了其他语言特性的基础支持,例如接下来我们要探讨的更为重要的多态性,那么多态性究竟是如何定义,如何实现的呢,通过本节的学习后我们将对其有较为深入的认知。

2.有关多态

2.1 多态的概念

● Java中多态性指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。简单说多态是具有表现多种形态的能力的特征
● 发送消息就是方法调用。
● 现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在IE界面下弹出的浏览器的帮助文档;如果当前在 Word 下弹出的就是 office帮助;在 Windows 下弹出的就是 Windows 帮助和支持

2.2 多态的语法

大类型 引用名 = new 小类型(..);
换算:  父类类名 引用名 = new 子类类名(..);

2.3 多态的作用

●同一个事件发生在不同的对象上会产生不同的结果,因此,多态的主要作用适用于消除类型之间的耦合关系,提高程序的可扩展性。

3.多态的分类

3.1 方法多态

● 方法重写与方法重载也是体现多态。
● 调用同一个方法,执行的逻辑是不一样的。

public class Demo{
    
    //方法重载
    public void fun1(){
        System.out.println("fun1................");
    }
    public void fun1(String str){
        System.out.println("fun2..............");
    }
}
@Override
public void show() {
	System.out.println(this.getName() + "钻火圈.....");
}

@Override
public void show() {
	System.out.println(this.getName() + "正在表演骑自行车");
}

3.2 对象多态

● 下面的案例,重点分析对象多态。

4. 对象多态

● 前提:要有层级关系。(在继承的关系下,或者是在接口的实现关系下)
● 对象在代码级别中出现的场景:
1. 创建对象
2. 形式参数
3. 返回值
4. 对象数组

4.1 案例_喂宠物(形参)

在正常生活中,我们可能会饲养多个宠物。使用面向对象相关内容,实现此功能。

1. 创建类

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Pet {

    private String name;
    private int age;


    public void cute() {
        System.out.println(name + "正在撒娇......");
    }

    public void bark() {
        System.out.println(name + "正在叫.......");
    }
}
public class Cat extends Pet {
    public Cat() {
		super();
    }

    public Cat(String name, int age) {
        super(name, age);
    }

    //方法重写

    @Override
    public void bark() {
        System.out.println(getName() + "正在喵喵叫......");
    }
}
public class Dog extends Pet {

    public Dog() {
    }

    public Dog(String name, int age) {
        super(name, age);
    }

    @Override
    public void bark() {
        System.out.println(getName() + "在汪汪的叫......");
    }
}

2. 喂宠物

2.1 方法重载
//主人给每个小宠物喂食
public class Master {

    //有一个方法  给宠物喂食
    public static void feed(@NonNull Cat c) {
        System.out.println("主人正在给" + c.getName() + "喂食");
        //宠物会发出叫声
        c.bark();
    }
    
    public static void feed(@NonNull Dog c) {
        System.out.println("主人正在给" + c.getName() + "喂食");
        //宠物会发出叫声
        c.bark();
	}
}

● 从功能角度分析,方法重载可以实现需要的功能。
● 出现的问题:
○ 1. 每新增一个新的子级类型,都会在Master新增一个新的feed方法。
○ 2. feed方法代码冗余
○ 3. 没有满足程序设计的开放封闭原则(对新增开放,对修改关闭)
○ 4. 多次修改源代码,报错的概率就会提升

2.2 多态实现(使用场景)

核心: 多态开发或者设计程序,不能面向具体类型开发,应该面向父级开发。

public static void feed(@NonNull Pet pet) {
	System.out.println("主人正在给" + pet.getName() + "喂食");
	//宠物会发出叫声
	pet.bark();
}
public class Test{

	public static void main(String[] args){

		Cat cat = new Cat("加菲猫", 2);
		Dog dog = new Dog("旺财", 3);

		Master.feed(cat);
		Master.feed(dog);
	}
}

4.2 创建对象

也可以在创建子类对象的时候这样编写:

public class Test{
	public static void main(String[] args){
		
		//体现的也是多态
		//子类对象向上转型   父类引用指向任意一个具体的子类对象
		//pet1,pet2是多态的
		Pet pet1 = new Cat("加菲猫", 2);
		Pet pet2 = new Dog("旺财", 3);
		
		Master.feed(pet1);
		Master.feed(pet2);
	}

4.3 返回值类型

主要体现在方法重写中,子类重写的方法的返回值类型<=父类方法的返回值类型。

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Pet {

    private String name;
    private int age;
    
    public Pet demo(){
        return null;
    } 
}
public class Cat extends Pet {
    public Cat() {
    }

    public Cat(String name, int age) {
        super(name, age);
    }


    //重写demo
    @Override
    public Monkey demo() {
        return null;
    }
}

4.4 对象数组

使用数组维护多个小宠物对象,以及循环遍历打印宠物的名称。

public class Test{

	public static void main(String[] args){

		//创建多个子类对象  使用一个数组维护多个不同的子类对象
		Cat cat = new Cat("加菲猫", 2);
		Dog dog = new Dog("旺财", 3);
		Fox daJi = new Fox("妲己", 1000);

		//数组语法:  数据类型[] 名称 = new 数据类型[length];
		Pet[] petArray = new Pet[10];
		petArray[0] = cat;
		petArray[1] = new Dog("旺财", 3);
		petArray[2] = new Fox("妲己", 1000);

		Pet[] pets = {
			new Cat("加菲猫", 2),
			new Dog("旺财", 3),
			new Fox("妲己", 1000)
			};

		//遍历输出每一个子类对象的name  循环遍历数组元素
		for (Pet p : petArray) {
			if (p == null) break;
			System.out.println(p.getName());
		}
	}
}

5.多态总结

//不管是在形参,在返回值,在创建对象的时候,只要我们这样编写代码: 

Pet  pet = cat;  Pet  pet  =  new Cat("加菲猫", 2);

多态 <==>向上转型

  1. 父类引用多态: pet是父类类型的引用 底层真正的指向的是具体的子类类型 (父类引用指向任意一个具体的子类对象)
  2. 向上转型: 父与子 具体子类对象向上转型成父类引用
  3. 编译时数据类型 与 运行时数据类型不一致的时候 也是多态的体现

编译时数据类型: jvm加载认知的class文件 = 左边的类型 Pet.class
运行时数据类型: =右边的数据类型 Cat.class

6.向下转型

● 复习基本数据类型的转换:
1、 自动转换(隐式转换)
2、 强制转换(手动转换)
其实对于引用数据类型而言,这种思想也是适合的。
一般我们在引用数据类型里面,我们成为数据类型的转换为: 向上转型 和 向下转型。

有关 instanceof

● Instanceof 判断一个对象是否是一个类的实例。这样就可以避免出现类型转换的异常了。

作用:判断当前对象与指定类型是否兼容, 返回布尔类型. true-兼容, false-不兼容

语法:引用名 instanceof 类名

public static double calSalary(Employee employee) {
	//有没有子类?  employee就是子类对象。

	double salary = employee.getSalary();
	if (employee instanceof PM) {// 判断employee是否是PM类对象
		PM pm = (PM) employee;
		//父转子 (父底层就是子类对象)  遵循强转的语法  向下转型  有可能会出现 ClassCastException
		salary += pm.getBonus();
	}
	return salary;
}

7.抽象类

7.1 为什么使用抽象类

● 为抽象类用于定义通用行为、强制实现、代码复用、部分实现和框架设计,适合需要部分共享功能且要求子类实现特定方法的场景。

1.1 代码级别解释

● 从代码级别
● 如果子类都重写父类的某个/某些方法
● 这个时候父类里面方法体逻辑没有任何意义的。---->想法: 可以将父类的方法变为一个没有方法体的方法
● 有2种方式可以实现以上需求:
1.1 native方法

  1. 子类可以不用重写。(在编译期间不会报错)
  2. 调用native的方法 运行期间可能会报错。error。
    2.1 要么引入第三方语言开发的接口调用native的方法
    2.2 要么子类全部重写native的方法
    1.2 abstract方法
  3. 一个类中 有抽象方法 这个类必须是一个抽象类 abstract class
  4. 一个类中 有抽象方法 这个类还有子类 >在编译期间都会报错
    2.1 子类变为抽象类
    2.2 子类都重写父类的抽象方法
    ●通过抽象方法引出抽象类。
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public abstract class Teacher {

    private int id;
    private String name;
    private int age;

    public void introduce() {
        System.out.println(this.toString());//继承关系下  this: 子类对象
    }
    //抽象方法: 没有方法体。 就是规则。针对于子类,要求子类全部重写父类里面提供的抽象方法
    //有可能会报错: 一个类中出现了抽象方法  这个类肯定是个抽象类
    public abstract Object teach();

    //满足里氏替换原则
    //1. 父类出现的任意一个地方都可以使用子类替换
    //2. 子类不要重写父类的方法
    //3. 如果要重写  尽可能使用抽象类以及接口。
}

1.2 程序设计解释

从程序设计/开发规范(习惯)角度进行解释
抽象类: 对类的抽象。----> 父类 -----> 抽象类肯定也是作为父类使用。
在继承关系下 一般不会创建父类对象

7.2 语法

● 抽象类语法:

abstract 访问修饰符 class 类名{}
访问修饰符 abstract class 类名{}: 任意位置,修饰符之间的顺序不做要求

使用

  1. 无法实例化对象
  2. 抽象类依旧可以参与多态作为引用声明
  3. 抽象类必须为父类
  • 非父类的类声明为抽象后, 其内部内容无法被使用, 会导致类无效
  1. 抽象父类依旧存在构造, 作用为供子类构造使用
    抽象父类必须根据子类需求完整提供构造

● 抽象方法语法 :

访问修饰符 abstract 返回值类型 方法名(形参列表);

使用

  1. 只有声明部分, 没有实现部分
  2. 抽象方法与抽象类的关系:
  • 抽象方法必须存在于抽象父类
  • 抽象父类可以存在非抽象方法
  1. 子类必须重写抽象父类中的抽象方法
  • 除非子类自身也是抽象父类
  1. 抽象父类中的行为方法是否应为抽象方法的判断标准:
  • 子类是否会使用父类方法体, 用则不抽象, 不用则抽象
  • 是否需要约束子类重写方法, 需要则抽象, 不需要则不抽象

● 简单的理解,Java的抽象就是只声明行为接口(方法签名)而不完成具体的实现。

7.3 组成部分

@Setter
@Getter
public abstract class MyAbstractClass { //一般都是作为父类

    //1. 抽象类中不一定有抽象方法
    //2. 一个类中 有抽象方法  这个类必须是一个抽象类

    //组成部分
    private String name;

    private static void demo() {

    }

    //抽象方法: 没有方法体的方法-----> 必须要实现----> 子类实现
    //abstract可以与哪些修饰符综合使用?  public protected 默认 
    public abstract void demo1();

    protected abstract void demo2();

    abstract void demo3();
    
    //构造方法
    public MyAbstractClass(String name) {
        this.name = name;
    }

}

7.4 作用

● 作为父类使用,定义子类都具备的行为。
● 功能与普通类一致,只是多了一个抽象方法。
● 解决子类之间的冗余问题
● 约束子类拥有某些内容
约束子类重写某些方法

8.5 总结

● 利用抽象的概念,能够在开发项目中创建扩展性很好的架构,优化程序
● 抽象类,抽象方法,在软件开发过程中都是设计层面的概念。也就是说,设计人员会设计出抽象类,抽象方法,程序员都是来继承这些抽象类并覆盖抽象方法,实现具体功能
抽象类是抽象方法的容器,如果某个类中包含有抽象方法,那么该类就必须定义成抽象类。抽象类中也可以包含有非抽象的方法甚至抽象类中可以没有抽象方法(只要一个类不应该有具体对象,就应该是抽象类,不一定是有抽象方法的)
抽象类不可以直接实例化,只可以用来继承作为其他类的父类存在
抽象类的派生子类应该提供对其所有抽象方法的具体实现
● 如果抽象类的派生子类没有实现其中的所有抽象方法,那么该派生子类仍然是抽象类,只能用于继承,而不能实例化,但可以有构造函数(用于帮助子类快速初始化共有属性)

8.内部类

作用

打破封装, 又不破坏封装

身为一个独立的类, 可以随意访问外部类的私有内容, 打破了外部类的封装

同时也身为外部类的组成部分之一, 并不破坏外部类对其他类的限制

“类的单一职责”: 类的功能和作用应该是单一的, 才能更好的进行扩展和维护

内部类可以在访问外部类内容书写功能的基础上不影响外部类的单一职责, 优化程序设计

分类

  1. 成员内部类 (了解)
  2. 静态内部类 (了解)
  3. 局部内部类 (熟悉)
  4. 匿名内部类 (掌握)

8.1 成员内部类

[访问修饰符] class 成员内部类类名{
    
}: 访问修饰符为外部类添加设置, 作用为限制外界使用自身内部类时的可用范围,四个访问修饰符都可用

使用

  1. 位置: 类以内, 与外部类的属性方法平级

  2. 是实例状态

  3. 高版本中, 可以定义任意内容

    • 偏低版本中, 无法定义静态内容

    为了保证数据状态的一致性, 内部通常都是实例内容

  4. 当出现重名时:

    • 内部类局部变量: 变量名
    • 内部类属性: this.属性名
    • 外部类属性:外部类类名.this.属性名
  5. 对象创建需要借助外部类对象

    外部类类名.内部类类名 引用名 =  外部类对象名.new 内部类类名();
    
public class Outer1 {
    private String str = "这是外部类实例属性str";

    //成员内部类
    public class Inner1{
        static String s = "这是成员内部类的静态内容";
        String str = "这是内部类实例属性str";

        public void ma(){
            String str = "这是内部类的局部变量str";
            System.out.println(str);//这是内部类的局部变量str
            System.out.println(this.str);//这是内部类实例属性str
            System.out.println(Outer1.this.str);//这是外部类实例属性str

        }
    }

}
public class TestOuter1 {
    public static void main(String[] args) {
        //先创建外部类对象
        Outer1 o1 = new Outer1();
        //再内部类对象
        Outer1.Inner1 inner1 = o1.new Inner1();
        //调用内部类方法测试
        inner1.ma();
    }
}

8.2 静态内部类

[访问修饰符 static] class 静态内部类类名{
    
}: 访问修饰符与static都为外部类给内部类添加的设置

使用

  1. 位置与成员内部类一致

  2. 可以定义任意内容

    为了保证数据状态的统一, 通常定义都是静态内容

  3. 在内部类静态方法中, 可以通过类名.静态属性名直接区分获取重名属性

  4. 无法访问外部类实例内容

  5. 对象创建只需借助外部类类名

    外部类类名.内部类类名 引用名 = new 外部类类名.内部类类名(); 
    
  6. 可以通过外部类类名.内部类类名.静态内容的方式直接访问内部类内容

public class Outer2 {
    static String str = "外部类静态属性str";
    String s = "外部类实例属性s";

    //静态内部类
    public static class Inner2{
        static String str = "内部类静态属性str";
        public static void ma(){
            String str = "内部类局部变量str";
            System.out.println(str);
            System.out.println(Inner2.str);//内部类静态属性str
            System.out.println(Outer2.str);//外部类静态属性str

//            System.out.println(s); 报错
        }

        public void method(){
            System.out.println("内部类的实例方法method");
//            System.out.println(s); 报错
        }
    }
}
public class TestOuter2 {
    public static void main(String[] args) {
       //创建静态内部类对象
        Outer2.Inner2 inner2 = new Outer2.Inner2();
        //访问内部类中的方法
        inner2.ma();
        inner2.method();
        //直接访问内部类的静态内容
        Outer2.Inner2.ma();
    }
}

8.3 局部内部类

class 局部内部类类名{
    
}

使用

  1. 位置: 外部类方法内, 和外部类局部变量平级

  2. 作用范围: 定义行开始至直属代码块结束

  3. 只能在所属范围内创建对象

  4. 高版本中, 可以定义任意内容

  5. 只能访问平级的常量:

    • JDK7.0前: 必须为通过final修饰的常量
    • JDK7.0后: 未二次改值的事实常量即可

    访问外部类属性时不做要求

  6. 只有所属方法被调用时, 局部内部类才有可能被执行

public class Outer3 {
    String str = "外部类属性";

    public void method(){
        str = "这是在更改属性值";
        String s = "这是外部类的局部变量";
       // s = "这是在更改局部变量的值"; 值一旦更改, Inner中的访问将会报错

        //局部内部类
        class Inner{
            public void ma(){
                System.out.println(str);
                System.out.println(s);
            }
        }
        //创建内部类对象
        Inner inner = new Inner();
        //调用内部类的方法
        inner.ma();

    }
}
public class TestOuter3 {
    public static void main(String[] args) {
       //创建外部类对象
        Outer3 o3 = new Outer3();
        //调用外部类方法, 测试局部内部类的内容
        o3.method();
    }
}

8.4 匿名内部类

作用

语法作用为创建一个子类|实现类对象, 匿名内部类内容 = 子类|实现类的类内容

匿名内部类只能简化以往的部分写法: 当一个子类|实现类只需实例化一次时

语法

父类类名|接口名 引用名 = new 父类类名|接口名(){
    //子类|实现类的类内容
};

在这里插入图片描述

使用

  1. 语法特点: 将类的声明 & 方法的实现 & 对象的创建三合一

  2. 匿名类的对象必须用多态接收

  3. 只能在执行语法时创建唯一的一次对象, 无法多次实例化

  4. 接收对象地址的引用类型: 与声明匿名类的类型一致 | 是声明匿名类的类型的大类型

  5. 内部可以定义独有内容, 但是只能在本类之中使用

    • 原因: 匿名类无法进行类型的强转
  6. 无法显式定义构造, 只存在一个默认的无参构造供创建唯一一次对象时调用

  7. 匿名内部类根据书写位置同时也身为成员内部类|局部内部类

8.5 lambda表达式 (了解)

下文详细介绍lambda的应用

接口的分类:

  1. 标记式接口: 内部无内容
  2. 常量式接口: 只声明了属性, 未定义方法
  3. 函数式接口: 只定义一个需要重写的方法
  4. 普通接口: 定义了多个需要重写的方法

匿名内部类-> 简化部分子类|实现类对象的书写

​ ↓

lambda表达式->简化部分匿名内部类的书写

作用

生成创建一个函数式接口的匿名实现类对象

语法
(重写方法的形参列表) -> {
    重写的方法体
}
使用引用接收对象:
接口名 引用名 = (形参列表)->{
   //方法的操作语句
};
使用
  1. JDK8.0之后
  2. lambda表达式创建的实现类对象身份由引用类型决定
  3. lambda只能创建一次匿名实现类对象, 无法多次实例化
  4. lambda的简化标准:
    • 参数列表的数据类型可省 - 要省都省
    • 当参数只有一个时, 小括号可省
    • 当操作语句只有一条时, 大括号可省
    • 当操作语句只有一条且为return语句时, 大括号和return关键字都可省 - 要省都省
public interface IA {
    void ma();
}

public interface IB {
    /**
     * 判断n是否为偶数并输出
     * @param n
     */
    void mb(int n);
}

public interface IC {
    /**
     * 计算参数之和并返回
     * @param a
     * @param b
     */
    int mc(int a,int b);
}
public class TestLambda1 {
    public static void main(String[] args) {
        //利用匿名内部类创建IA的实现类对象
        IA ia = new IA() {
            @Override
            public void ma() {
                System.out.println("匿名内部类-ma()");
            }
        };

        //利用lambda表达式创建IA的实现类对象
        IA ia2 = () -> {
            System.out.println("匿名内部类-ma()");
        };

        //简化
        IA ia3 = () ->System.out.println("匿名内部类-ma()");
    }
}
public class TestLambda2 {
    public static void main(String[] args) {
        //利用匿名内部类创建IB的实现类对象
        IB ib = new IB() {
            @Override
            public void mb(int n) {
                System.out.println(n % 2 == 0 ? "偶数" : "不是偶数");
            }
        };

        //利用lambda表达式创建IB的实现类对象
        IB ib2 = (int n) -> {
            System.out.println(n % 2 == 0 ? "偶数" : "不是偶数");
        };
        //简化
        IB ib3 = n -> System.out.println(n % 2 == 0 ? "偶数" : "不是偶数");
    }
}
public class TestLambda3 {
    public static void main(String[] args) {
        //利用匿名内部类创建IC的实现类对象
        IC ic = new IC() {
            @Override
            public int mc(int a, int b) {
                return a + b;
            }
        };
        //利用lambda表达式创建IC的实现类对象
        IC ic2 = (int c, int d) -> {
            return c+d;
        };
        //简化
        IC ic3 = (a, b) -> a + b;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值