【Java基础教程】(十四)面向对象篇 · 第八讲:多态性详解——向上及向下转型、关键字 final与 instanceof的作用~

Java基础教程之面向对象 · 第八讲

  • 本节学习目标
  • 1️⃣ final 关键字
    • 1.1 final类
    • 1.2 final方法
    • 1.3 final属性
  • 2️⃣ 多态性
    • 2.1 向上转型
    • 2.2 向下转型
    • 2.3 关键字 instanceof
  • 总结

在这里插入图片描述

本节学习目标

  • 掌握final 关键字的主要作用及使用;
  • 掌握对象多态性的概念以及对象转型的操作;
  • 掌握instanceof 关键字的主要作用及使用;

1️⃣ final 关键字

在Java 中 final称为终结器,在Java 里面可以使用 final定义类、方法和属性,用于表示不可变性

  • final类:当一个类被声明为final时,意味着该类不能被继承。换句话说,不能创建该类的子类。通常将类声明为final的主要原因是防止其他类修改或扩展该类的行为。例如,java.lang.String就是一个被声明为final的类;
  • final方法:当一个方法被声明为final时,意味着该方法不能被子类重写或覆盖。这种限制可以保护方法的实现,确保不会被修改。通常情况下,一个方法被声明为final是因为它的实现在父类中已经足够完善,不希望子类对其进行修改;
  • final变量:当一个变量被声明为final时,意味着该变量的值不能被更改。一旦给final变量赋值,就不能再修改它的值。通常将变量声明为final是为了防止其被重新分配或重新赋值,保持其不可变性。final变量可以是基本类型(如intdouble等)或引用类型(如对象、数组引用等),但引用类型的final变量指的是不能被重新分配,即不能再指向其他对象,但仍然可以修改对象的状态。

使用final关键字可以带来以下好处:

  • 提高性能:final关键字可以使编译器进行优化,因为它知道这些元素不会被改变,所以可以进行一些优化处理。
  • 增加安全性:final关键字可以防止意外修改或覆盖。当某个类、方法或变量对于某种场景下应该保持固定不变时,使用final可以提供更强的安全性和可靠性。
  • 支持设计决策:通过将类、方法或变量声明为final,可以明确表达出它们在设计中的意图和限制。

1.1 final类

使用 final 定义的类不能再有子类,即:任何类都不能继承以 final 声明的父类。

//	范例 1: 观察 final 定义的类
final class A{}       // 此类不能够有子类

class B extends A{}  	// 错误的继承

此程序中由于 A 类在定义时使用了 final关键字,这样 A 就不能再有子类了,所以当 B 类继承 A 类时会在编译时出现语法错误。

需要注意的是,只是进行应用开发的话,那么大部分情况下不需要使用 final 来定义类。而如果是一些系统架构的代码开发时,才有可能会使用到这样的代码。同时要注意一点:String也是使用 final 定义的类,所以String类不允许被继承。

1.2 final方法

使用 final 定义的方法不能被子类所覆写。 在一些时候由于父类中的某些方法具备一些重要的特征,并且这些特征不希望被子类破坏(不能够覆写), 就可以在方法的声明处加上final, 意思是子类不要去破坏这个方法的原本作用。

//	范例 2: 观察 final 定义方法
class A{
	public final void fun(){}  // 此方法不允许子类覆写
}

class B extends A{
	public void fun(){}			//错误:此处不允许覆写
}

此程序在 A 类中定义的 fun()方法上使用了final进行定义,这就意味着子类在继承 A 类后将不允许覆写 fun()方法。

1.3 final属性

使用 final定义的变量就成为了常量,常量必须在定义的时候设置好内容,并且不能修改。

//	范例 3: 定义常量
class A{
	final double GOOD = 100.0;	//GOOD级别就是100.0

	public final void fun(){
		GOOD=1.1;                          //错误:不能够修改常量
	}
}

此程序使用 final 定义了一个常量 “GOOD”, 这就相当于利用 “GOOD” 这个名称代表了“100.0” 这个数据。所以代码中定义常量的最大意义在于:使用常量可以利用字符串(常量名称)来更直观地描述数据。

大家可以发现本处定义的常量名称使用了全部字母大写的形式(final double GOOD =100.0;),这是Java中的命名规范要求,这样做的好处是可以明确地与变量名称进行区分,开发中也应该遵守。

而在定义常量中还有一个更为重要的概念 — — 全局常量,所谓全局常量指的就是使用了"public"、“static”、“final”3个关键字联合定义的常量,例如:

public static final String MSG="YOOTK";

static 修饰的数据保存在公共数据区,所以此处的常量就是一个公共常量。同时一定要记住,在定义常量时必须对其进行初始化赋值,否则将出现语法错误。

2️⃣ 多态性

前面文章中已经详细解析了面向对象的封装性、继承性两大特征,而多态是面向对象的最后一个主要特征,也是一个非常重要的特性,掌握了多态性才可以编写出更加合理的面向对象程序。而多态性在开发中可以体现在以下两个方面:

  • 方法的多态性:重载与覆写;

    • 方法重载:同一个方法名称,根据不同的参数类型及个数可以完成不同的功能;
    • 方法覆写:同一个方法,根据实例化的子类对象不同,所完成的功能也不同。
  • 对象的多态性:父子类对象的转换。

    • 向上转型:子类对象变为父类对象,格式:父类 父类对象 = 子类实例,自动类型转换;
    • 向下转型:父类对象变为子类对象,格式:子类 子类对象 = (子类) 父类实例,强制类型转换。

对于方法的多态性在之前已经有了详细地阐述,所以本节主要介绍对象多态性,有一点需要注意的是,对象多态性和方法覆写是紧密联系在一起的。

当然,要想真正理解多态性是如何去应用的,多态性更合理的解释需要结合抽象类与接口来一起讲解,下一篇文章将会为大家介绍抽象类与接口等知识,而要想充分理解这一概念也需要更多时间的了解与沉淀。

//	范例 4: 观察如下程序
class A{
	public void print(){
		System.out.println("A 、public void print(){}");
	}
}

class B extends A{
	public void print(){  //  此时子类覆写了父类中的print()方法
		System.out.println("B 、public void print(){}");
	}
}

public class TestDemo {
	public static void main(String args[]){
		B b = new B();                       //实例化的是子类对象
		b.print();                                  //调用被子类覆写过的方法
	}
}

程序执行结果:

B 、public void print(){}

此程序在方法覆写中已经讲解过了,由于现在子类B 覆写了A 类中的 print(), 并且在主方法中实例化的是子类对象,这样当调用 print()方法时调用的一定是已经被覆写过的方法。也就是说在本程序中需要观察以下两点。
(1)观察实例化的是哪个类的对象:“new B()”;
(2)观察调用的方法是否被子类所覆写,如果覆写了,则调用被覆写过的方法,否则调用父类方法。

2.1 向上转型

那么这样的概念与对象的多态性有什么关联呢?下面对上边案例的主方法进行一些变更,以观察对象的向上转型操作。

//	范例 5: 对象向上转型(自动完成)
public class TestDemo {
	public static void main(String args[]){
		A a = new B();  // 实例化的是子类对象,对象向上转型
		a.print();     	//调用被子类覆写过的方法
	}
}

程序执行结果:

B 、public void print(){}

可以看到此程序的执行结果与范例4 的程序执行结果没有任何区别,然而在本程序中发生了对象的向上转型操作 (A a = new B();),并且最终由父类对象调用了 print() 方法,但是最终的执行结果却是被子类所覆写过的方法的执行结果。

而产生这样结果的原因也很好理解,在范例4 中已经重点强调过:根据实例化对象所在类是否覆写了父类中的方法来决定最终执行,此程序实例化的是子类对象 (new B()),并且 print() 方法又被子类覆写了,那么最终所调用的一定是被覆写过的方法。

实际上通过此程序大家可以发现对象向上转型的特点,整个操作中根本不需要关心对象的声明类型,关键在于实例化新对象时所调用的是哪个子类的构造,如果方法被子类所覆写,调用的就是被覆写过的方法,否则就调用父类中定义的方法。这一点与方法覆写的执行原则是完全一样的。

2.2 向下转型

在清楚了对象的向上转型操作及特点后,下面再来观察对象的向下转型操作。

//	范例 6: 对象向下转型
public class TestDemo {
	public static void main(String args[]){
		A a = new B();		//实例化的是子类对象,对象向上转型 
		B b = (B) a;		//对象需要强制性地向下转型
		b.print();			//调用被子类覆写过的方法
	}
}

程序执行结果:

B 、public void print(){}

此程序首先利用对象的向上转型实例化了父类A 的对象,然后将此对象进行向下转型为子类 B 的对象,由于整个代码中关键字 new 调用的是子类 B 的构造 (new B()),所以调用的是被子类 B 所覆写的 print() 方法。

因为有强制性转换的操作,所以向下转型操作本身是有前提条件的,即必须发生向上转型后才可以发生向下转型。如果是两个没有关系的类对象发生强制转换,就会出现 “ClassCastException” 异常。

//	范例 7: 错误的转型操作
public class TestDemo {
	public static void main(String args[]){
		A a = new A(); 		//直接实例化对象, 此时并没有发生子类对象向上转型的操作,所以强制转型会带来安全隐患
		B b = (B)a;      	//强制向下转型,此处产生“ClassCastException”异常
		b.print();          //此语句无法执行
	}
}

程序执行结果:

Exception in thread "main" java.lang.ClassCastException: A cannot be cast to B at TestDemo.main(TestDemo.java:29)

本程序出现的异常表示的是类转换异常,指的是两个没有关系的类对象强制发生转型时所带来的异常。因为此时并没有发生向上转型,所以向下转型是会存在安全隐患的,开发中应该尽量避免此类操作。

对象多态性的本质是根据实例化对象所在的类是否覆写了父类中的指定方法来决定最终执行的方法体,那么这种向上或向下的对象转型有什么意义呢?

在实际开发中,对象向上转型的主要意义在于参数的统一,也是最为重要的用法,而对象的向下转型指的是调用子类的个性化操作方法。

//	范例 8: 对象向上转型作用分析
class A {
	public void print(){
		System.out.println("A、public void print(){}");
	}
}

class B extends A{    //定义A的子类
	public void print(){	//此时子类覆写了父类中的print()方法
		System.out.println("B、public void print(){}");
	}
}

class C extends A {    //定义A的子类
	public void print(){	//此时子类覆写了父类中的print()方法
		System.out.println("C、public void print(){}");
	}
}

public class TestDemo {
	public static void main(String args[]){
		fun(new B());    //对象向上转型,等价于:A a=new B();
		fun(new C());    //对象向上转型,等价于:A a=new C();
	}
	
	/**
	* 接收A类或其子类对象,不管传递哪个对象都要求调用print()方法
	* @param a A类实例化对象
	*/
	public static void fun(A a)(
		a.print();
	}
}

程序执行结果:

B、public void print(){}
C、public void print(){}

在此程序的 fun() 方法上只是接收了一个 A 类的实例化对象,按照对象的向上转型原则,此时的 fun() 方法可以接收 A类对象或所有A类的子类对象,这样只需要一个A类的参数类型,此方法就可以处理一切 A的子类对象 (即便A类有几百万个子类,fun() 方法依然可以接收及处理 )。 而在 fun() 方法中将统一调用 print() 方法,如果此时传递的是子类对象,并且覆写过 print()方法,就表示执行被子类所覆写过的方法。

如果说向上转型是统一调用的参数类型,那么向下转型就表示要执行子类的个性化操作方法。实际上当发生继承关系后,父类对象可以使用的方法必须在父类中明确定义,例如:此时在父类中存在一个 print() 方法,哪怕这时此方法被子类覆写过,父类对象依然可以调用。但是如果子类要扩充一个 funB()方法,这个方法父类对象并不知道, 一旦发生向 上转型,那么 funB()方法父类对象肯定无法使用。

//	范例 9: 子类扩充父类方法
class A{
	public void print(){
		System.out.println("A、public void print(){}");
	}
}

class B extends A{  //定义A的子类
	public void print(){	//此时子类覆写了父类中的print()方法
		System.out.println("B、public void print(){}");
	}
	
	/**
	* 在子类中扩充一个新的方法,但是此方法只能由子类对象调用,父类对象不能调用 
	*/
	public void funB(){
		System.out.println("B、扩充的funB()方法");
	}
}

此程序在子类B中定义的 funB() 方法在子类对象发生向上转型时( A a = new B();),父类对象将无法调用,也就是说这个方法是子类自己的特殊功能,并没有在父类中定义,如果此时要想调用子类中的方法,就必须将父类对象向下转型。

//	范例 10: 向下转型,调用子类中的特殊功能
public class TestDemo {
	public static void main(String args[]){
		fun(new B());//向上转型,只能调用父类中定义的方法
	}
	public static void fun(A a){
		B b = (B)a;  //要调用子类的特殊操作,需要向下转型
		b.funB();    //调用子类的扩充方法
	}
}

程序执行结果:

B、扩充的funB()方法

此程序如果要调用 fun()方法,则子类B的实例化对象一定要发生向上转型操作,但是这个时候父类对象无法调用子类BfunB()方法,所以需要进行向下转型才能正常调用 funB()方法。但是如果每一个子类都去大量扩充自己的新功能,这样就会严重破环开发的参数统一性,所以方法应该以父类为主,子类可以覆写父类方法,但尽量不要扩充新的方法。

通过以上所有分析可以发现如下特点:
向上转型:其目的是参数的统一,但是向上转型中,通过子类实例化后的父类对象所能调用的方法只能是父类中定义过的方法;
向下转型:其目的是父类对象要调用实例化它的子类中的特殊方法,但是向下转型是需要强制转换的,这样的操作容易带来安全隐患。

以个人经验来说,对于对象的转型,实际上 80%的情况下都只会使用向上转型,因为可以得到参数类型的统一,方便于程序设计;并且子类定义的方法大部分情况下应该以父类的方法名称为标准进行覆写,而不要过多地扩充方法。5% 的情况下会使用向下转型,目的是调用子类的特殊方法。还有15%的情况下是不转型,例如:String

2.3 关键字 instanceof

为了保证转型的顺利进行,Java 提供了一个关键字:instanceof,利用此关键字可以判断某一个对象是否是指定类的实例,使用格式如下。

//	返回boolean型
对象 instanceof

如果某个对象是某个类的实例,就返回 true, 否则返回 false

//	范例 11: 使用instanceof 判断
public class TestDemo  {
	public  static void main(String  args[]){
		A a = new B();               //对象向上转型
		System.out.println(a instanceof A);
		System.out.println(a instanceof B);
		System.out.println(null instanceof A);
	}
}

程序执行结果:

true
true
false

此程序利用 instanceof 判断了某个对象是否是指定类的实例,通过程序的执行结果可以发现 a 对象由于采用了向上转型进行实例化操作,所以 aA 类或 B类的实例化对象,而 null 在使用 instanceof 判断时返回的结果为 false

既然 instanceof 关键字可以准确地判断出实例化对象与类的关系,那么就可以在进行对象强制转换前进行判断,以保证安全可靠的向下转型操作。

从实际的开发来讲,向下转型的操作几乎是很少见到的,但是如果真的出现了,并且开发者不确定此操作是否安全时,一定要先使用instanceof 关键字判断。

//	范例 12: 使用 instanceof  判断
class A{
	public void print(){
		System.out.println("A、public void print(){}");
	}
}

class B extends A{                         //定义A 的子类
	public void print(){               //此时子类覆写了父类中的print()方法
		System.out.println("B、public void print(){}");
	}
	public void funB(){
		System.out.println("B、扩充的funB()方法");
	}
}

public class TestDemo {
	public static void main(String args[]){
		fun(new B());
	}
	public static void fun(A a){ //对象向上转型
		a.print();
		if (a instanceof B){ //如果 a 对象是B 类的实例
			B b= (B)a;		//向下转型
			b.funB();		//调用子类扩充的方法
		}
	}
}

程序执行结果:

B、public void print(){}
B、扩充的funB()方法

在此程序中为了保证安全的向下转型操作,在将父类转换为子类对象时首先使用了 instanceof 进行判断,如果当前对象是子类实例,则进行强制转换,以调用子类的扩充方法。

总结

在本文中我们学习了Java中两个重要的概念:final关键字和多态性。

首先,我们了解到final关键字可以应用于类、方法和属性。final类表示不可继承,final方法表示不可覆盖,而final属性表示不可修改。使用final关键字可以提高代码的安全性和性能,并明确表达设计意图。

其次,我们探讨了多态性的概念。多态性是面向对象编程的重要特性,其中包括向上转型、向下转型和关键字instanceof。通过向上转型,我们可以实现参数的统一和代码的灵活性。而向下转型则允许调用子类的个性化操作方法。关键字instanceof则可用于检查对象是否属于某个类或其子类的实例。

理解final关键字和多态性对于Java开发至关重要。它们能帮助我们设计更安全、灵活且易于维护的代码。合理运用final关键字,可以防止不必要的修改和继承;深入理解多态性,可以提高代码的可扩展性和可复用性。

最后,我们需要注意在使用final关键字时要谨慎,并根据具体需求进行选择。同时,熟练掌握多态性的概念和技巧,有助于编写更具有弹性和适应性的代码。


温习回顾上一篇(点击跳转)《【Java基础教程】(十三)面向对象篇 · 第七讲:继承性详解——继承概念及其限制,方法覆写和属性覆盖,关键字super的魔力~》

继续阅读下一篇(点击跳转)《【Java基础教程】(十五)面向对象篇 · 第九讲:~》

你可能感兴趣的:(#,Java基础教程,java,开发语言,jvm,经验分享,java-ee,后端)