17、Java基础教程之面向对象·第九讲

  • 本节学习目标
  • 1️⃣ 抽象类
    • 1.1 抽象类定义
  • 1.2 抽象类的相关限制
  • 1.3 抽象类应用——模板设计模式
  • 2️⃣ 接口
    • 2.1 接口定义
  • 2.2 接口的应用——标准
  • 2.3 接口的应用——工厂设计模式 (Factory)
  • 2.4 接口的应用——代理设计模式 (Proxy)
  • 3️⃣ 抽象类与接口的区别
  • * 总结

*

本节学习目标

  • 掌握 抽象类和接口的定义、使用、区别、常见设计模式;

1️⃣ 抽象类

抽象类是代码开发中的重要组成部分,利用抽象类可以明确地定义子类需要覆写的方法,这样相当于在语法程度上对子类进行了严格的定义限制,代码的开发也就更加标准。下面具体介绍抽象类的概念。

1.1 抽象类定义

普通类可以直接产生实例化对象,并且在普通类中可以包含构造方法、普通方法、static 方法、常量、变量。而所谓抽象类就是指在普通类的结构里面增加抽象方法的组成部分,抽象方法指的是没有方法体的方法,同时抽象方法还必须使用 abstract 关键字进行定义。拥有抽象方法的类一定属于抽象类,抽象类要使用 abstract 声明。

所有的普通方法上面都会有一个"{...}",来表示方法体,有方法体的方法一定可以被对象直接调用。抽象类中的抽象方法没有方法体,声明时不需要加“{}”,但是必须有 abstract 声明,否则在编译时将出现语法错误。

//	范例 1: 定义抽象类
abstract class A{
   
      		//定义一个抽象类,使用abstract声明
	public void fun(){
   
      	//普通方法
		System.out.println("存在有方法体的方法!");
	}
	//此方法并没有方法体的声明,并且存在abstract关键字,表示抽象方法
	public abstract void print();
}

在此程序的类中定义了一个抽象方法 print(),既然类中有抽象方法,那么类就必须定义为抽象类, 所以使用了 “abstract class”来定义。但是一定要记住:抽象类只是比普通类多了抽象方法的定义,其他结构与普通类完全一样。

按照传统的思路,既然已经实例化好了抽象类,那么就应该通过实例化对象来操作,但是抽象类是不能直接进行对象实例化操作的。

//	范例 2: 错误的实例化抽象类对象的操作
public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		A a = new A();       //A是抽象的,无法实例化
	}
}

此程序的代码在编译时就会出现错误,也就是说抽象类不能进行直接的对象实例化操作。不能够实例化的原因很简单:当一个类的对象实例化后,就意味着这个对象可以调用类中的属性或方法,但是在抽象类里面存在抽象方法,抽象方法没有方法体,没有方法体的方法怎么可能去调用呢? 既然不能调用方法,那么又怎么去产生实例化对象呢?

范例1中抽象类已经被成功地定义出来,但是如果要想使用抽象类,则必须遵守如下原则。
(1)抽象类必须有子类,即每一个抽象类一定要被子类所继承 (使用 extends 关键字),但是在 Java 中每一个子类只能够继承一个抽象类,所以具备单继承局限;
(2)抽象类的子类要么覆写抽象类中的全部抽象方法(强制子类覆写),要么也是抽象类;
(3)依靠对象的向上转型概念,可以通过抽象类的子类完成抽象类的实例化对象操作。

//	范例 3: 正确使用抽象类
abstract class A {
   
     		//定义一个抽象类,使用abstract声明
	public void fun(){
   
     	//普通方法
		System.out.println("存在有方法体的方法!");
	}
	
	//此方法并没有方法体的声明,并且存在abstract关键字,表示抽象方法
	public abstract void print();
}

//一个子类只能够继承一个抽象类,属于单继承局限
class B extends A{
   
                          //B 类是抽象类的子类,并且是一个普通类
	public void print(){
   
                    //强制要求覆写的方法
		System.out.println("Hello World!");
	}
}

public class TestDemo{
   
     
	public static void main(String args[]){
   
     
		A a = new B();              //向上转型
		a.print();                	//被子类覆写过的方法
	}
}

此程序为抽象类定义了一个子类 B, 而子类 B 是一个普通类,必须要覆写抽象类中的全部抽象方法,而在主方法中依靠子类对象的向上转型实现了抽象类 A 对象的实例化操作,而调用的 print()方法由于被子类所覆写,所以最终调用的是在子类 B 中覆写过的 print()方法。

在使用普通类的继承操作中,都是由子类根据约定(非语法限制)的方式实现的覆写,而抽象类的子类却可以在语法程度上强制子类的覆写,这一点感觉抽象类要比普通类更加严谨,那么在开发中应该继承普通类还是继承抽象类呢?

虽然一个子类可以去继承任意一个普通类,但是从开发的实际要求来讲,普通类不要去继承另外一个普通类,而要继承抽象类。

相比较开发的约定,开发者更愿意相信语法程度上给予的限定。很明显,强制子类去覆写父类的方法可以更好地进行操作的统一,所以对于抽象类与普通类的对比,有如下几点总结。

  • 抽象类继承子类里面会有明确的方法覆写要求,而普通类没有;
  • 抽象类只比普通类多了一些抽象方法的定义,其他的组成部分与普通类完全一样;
  • 普通类对象可以直接实例化,但是抽象类的对象必须经过向上转型后才可以得到 实例化对象。

1.2 抽象类的相关限制

抽象类的组成和普通类组成的最大区别只是在抽象方法的定义上,但是由于 抽象类和普通类使用以及定义的区别,如下概念可能会被读者所忽略,下面依次说明。

(1)抽象类里面由于会存在一些属性,那么在抽象类中一定会存在构造方法,目的是为属性初始化,并且子类对象实例化时依然满足先执行父类构造再调用子类构造的情况
(2)抽象类不能使用 final定义,因为抽象类必须有子类,而 final定义的类不能有子类;
(3)抽象类中可以没有任何抽象方法,但是只要是抽象类,就不能直接使用关键字new 实例化对象。

//	范例 4: 没有抽象方法的抽象类
abstract class A{
   
                            //定义一个抽象类
	public void print(){
   
                   //此为普通方法
		System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
	}
}

class X extends A{
   
                        //抽象类必须有子类
}

public class TestDemo{
   
     
	public static void main(String args[]){
   
     
		A a = new X();                    //通过子类实例化抽象类对象
		a.print();
	}
}

程序执行结果:

更多文章请访问:https://lst66.blog.csdn.net

本程序的抽象类 A 中没有定义任何抽象方法,但是按照 Java 的语法要求,此时的 A 类依然无法直接实例化,必须利用子类对象的向上转型才能为抽象类实例化对象。

(4)抽象类中依然可以定义内部的抽象类,而实现的子类也可以根据需要选择是否定义内部类来继承抽象内部类。

//	范例 5: 定义抽象类的内部类
abstract class A{
   
     		//定义一个抽象类
	abstract class B{
   
     	//定义内部抽象类
		public abstract void print();
	}
}

class X extends A{
   
     
	public void print(){
   
     
		System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
	}

	class Y extends B{
   
     	//定义内部抽象类的子类,此类不是必须编写
		public void print(){
   
     }	//方法覆写

	}	
}

此程序在抽象类A 中又定义了一个抽象类B, 而在定义 A 的子类X 时不一定非要定义内部类 Y。 当然也可以定义一个内部类Y, 这样可以直接继承内部的抽象类B

(5)外部抽象类不允许使用 static 声明,而内部的抽象类允许使用 static 声明,使用 static 声明的内部抽象类就相当于是一个外部抽象类,继承的时候使用“外部类.内部类”的形式表示类名称。

//	范例 6: 利用 static 定义的内部抽象类为外部抽象类
abstract class A {
   
     	//定义一个抽象类
	static abstract class B{
   
     	// static定义的内部类属于外部类
		public abstract void print();
	}
}

class X extends A.B{
   
               	//继承static 内部抽象类
	public void print(){
   
     
		System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
	}
}

public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		A.B ab = new X();           / / 向 上 转 型
		ab.print();
	}
}

程序执行结果:

更多文章请访问:https://lst66.blog.csdn.net

此程序利用 static在抽象类A 中定义了一个抽象类B,这样就相当于B 是一个外部类,则X 类就可以直接使用“A.B” 的名称继承这个外部类。

(6)在抽象类中,如果定义了static 属性或方法时,就可以在没有对象的时候直接调用。

//	范例 7: 在抽象类中定义 static 方法
abstract class A{
   
                             	//定义一个抽象类
	public static void print(){
   
              	// static 方法
		System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
	}
}

public class TestDemo{
   
     
	public static void main(String args[]){
   
     
		A.print();                      	//直接调用static方法
	}
}

程序执行结果:

更多文章请访问:https://lst66.blog.csdn.net

此程序在抽象类 A 中定义了一个 static 方法,由于 static 方法不受实例化对象的限制,所以可以直接由类名称调用。

利用static可以在抽象类中定义不受实例化对象限制的方法,那么就可以进一步的延伸。例如:现在抽象类只需要一个特定的系统子类操 作,那么就可以通过内部类的方式来定义抽象类的子类。

//	范例 8: 通过内部类的方式定义抽象类子类
abstract class A {
   
                   //定义一个抽象类
	public abstract void print(); //定义抽象方法

	private static class B extends A{
   
     	//内部抽象类子类
		public void print(){
   
              //覆写抽象类的方法
			System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
		}
	}

	public static A getInstance(){
   
         //此方法可以通过类名称直接调用
		return new B();
	}
}

public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		//此时取得抽象类对象时完全不需要知道B类这个子类存在
		A a = A.getInstance(); 
		a.print();	//调用被覆写过的抽象方法
	}
}

程序执行结果:

更多文章请访问:https://lst66.blog.csdn.net

此程序在抽象类 A中利用内部类 B进行子类继承,而后再调用,用户不需要知道抽象类的具体子类,只要调用了类中的“getInstance()”方法就可以取得抽象类的实例化对象,并且调用方法。这样的设计在系统类库中会比较常见,目的是为用户隐藏不需要知道的子类。

1.3 抽象类应用——模板设计模式

抽象类的最主要特点相当于制约了子类必须覆写的方法,同时抽象类中也可以定义普通方法,而且最为关键的是,这些普通方法定义在抽象类时,可以直接调用类中定义的抽象方法,但是具体的抽象方法内容就必须由子类来提供。

//	范例 9: 在抽象类的普通方法中调用抽象方法
abstract class A{
   
     		//定义一个抽象类
	public void fun(){
   
     	//此为普通方法
		this.print();	//在普通方法中直接调用抽象方法
	}
	
	public abstract void print();	//此为抽象方法
}

class X extends A{
   
     
	public void print(){
   
     
		System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
	}
}

public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		A a = new X();                      //通过子类实例化抽象类对象
		a.fun();                          	//抽象类中的普通方法
	}
}

程序执行结果:

更多文章请访问:https://lst66.blog.csdn.net

此程序在抽象类中的抽象方法 print()fun()方法直接调用,在定义抽象类A 的时候并不知道具体的子类是什么,但是只要是有子类,就必须明确地强制子类来覆写 print()方法,当调用 fun()方法时,执行的一定是被子类所覆写的抽象方法。

对于上边范例 9的程序,部分朋友可能会不理解,那么现在换个思路来思考。如果你是一个动物爱好者,当你见到动物你就会想办法让它叫,如果把叫这个操作理解为 fun()方法的话,那么具体的叫声就可以当作 print() 方法。由于动物多种多样,所以对于具体的叫声是根据每个动物的类来决定的。例如:狗的叫声是"汪汪"、猫的叫声是“喵喵",这些你都不需要去关注,你所关注的只是怎么触发让动物叫的操作,而具体怎么叫就由子类来决定。

按照以上的设计思路,实际上可以对程序做进一步的扩展,现在假设有以下3 类现实的事物(或者更多的事物)。

  • 机器人 (Robot):具备充电、工作两个基本操作;
  • 人类 (Human):具备吃饭、工作、睡觉三个基本操作;
  • 猪 (Pig):具备吃饭、睡觉两个基本操作。

现在要求实现对以上事物的控制,即可以控制机器人、人类、猪的操作行为,而控制的模式只具备 三个功能:吃 (eat())、 睡 (sleep())、 工作 (work())。

实际上大家可以发现,以上三类事物本身都具备一些相同的行为特征,例如:机器人、人类、猪都需要进行“”的操作,但是唯一的区别是机器人的吃实际上是充电的功能,本身也是属于补充能量的过程,而其他两类行为都是共同的。但是机器人不存在 休息(睡)的功能,即使让它休息也休息不了,而猪不具备工作的功能,即使让猪工作也是不可能的, 所以即使调用了这些操作,也应该不起任何作用。因此应该首先对行为进行抽象,然后每种行为可以创建出具体的子类,而每个子类的具体操作都应该由行为类的发出命令 ( command() 方法发出命令,是固定好的设计,所以应该为普通方法)。

那么此程序的类设计图如下图所示。

*
根据以上描述的代码实现如下:

//	范例 10: 定义一个行为类
abstract class Action  {
   
                	//定义一个抽象的行为类,行为不是具体的
	//定义常量时必须保证两个内容相加的结果不是其他行为,例如:EAT+SLEEP   的结果为6,不会和其他值冲突
	public static final int EAT=1;                //定义吃的命令
	public static final int SLEEP=5;           //定义睡的命令
	public static final int WORK=7;            //定义工作的命令

	/**
	*控制操作的行为,所有的行为都通过类中的常量描述,可以使用 EAT、SLEEP、WORK
	*或者进行命令的叠加使用,例如:边吃边工作,使用 EAT + WORK 来描述
	*@param flag 操作的行为标记
	*/
	public void command(int flag){
   
      
		switch (flag)		//switch 只支持数值判断,而if支持条件判断
			case EAT:		//当前为吃的操作
				this.eat();		//调用子类中具体的“吃”方法
				break;
			case SLEEP:		//当前为睡的操作
				this.sleep();	//调用子类中具体的“睡”方法
				break;
			case WORK:		//当前为工作的操作
				this.work();	//调用子类中具体的“工作”方法
				break;
			case EAT + WORK:	//行为组合,本处只是举例说明
				this.eat();		//调用“吃”的方法
				this.work();	//调用“工作”的方法
				break;
	}

	public abstract void eat();    //定义子类的操作标准
	public abstract void sleep(); 	//定义子类的操作标准
	public abstract void work();	//定义子类的操作标准
}	

在此程序的定义中,将具体的接收指令定义为 command() 方法,并且 command() 方法只接收固定的几个操作值(由具体的常量提供),同时该方法也支持操作的组合传递,而具体的操作行为不应该由行为这个类负责,而应由不同的子类覆写。

//	范例 10: 定义描述机器人的行为子类
class Robot extends Action{
   
         // 定义机器人行为
	public void eat(){
   
                              //覆写行为的操作
		System.out.println("机器人充能!");
	}
	public void sleep(){
   
                           //此操作不需要但必须覆写,所以方法体为空
	}
	public void work(){
   
                          //覆写行为的操作
		System.out println("机器人正在工作!");
	}
}

//	范例 10: 定义人的类
class Human extends Action{
   
        //定义人类行为
	public void eat(){
   
                            //覆写行为的操作
		System.out.println("人类正在吃饭!");
	}
	public void sleep(){
   
                         //覆写行为的操作
		System.out.println("人类正在睡觉休息!");
	}
	public void work(){
   
                         //覆写行为的操作
		System.out.println("人为了生存在努力工作!");
	}
}

//	范例 10: 定义猪的类
class Pig extends Action {
   
     
	public void eat(){
   
                              //覆写行为的操作
		System.out.printin("猪正在啃食槽!");
	}
	public void sleep(){
   
                         //覆写行为的操作
		System.out.println("猪在睡觉养膘!");
	}
	public void work(){
   
                             //此操作不需要但必须覆写,所以方法体为空
	}
}

以上三个类分别定义了3种事物的具体操作行为,但是由于抽象类的定义要求,所以每一个子类中即使不需要的操作方法也需要进行覆写,此时只要将它的方法体设置为空即可。

//	范例 10: 测试行为
public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		fun(new Robot());	//传递机器人行为子类
		fun(new Human());	//传递人类行为子类
		fun(new Pig());		//传递猪的行为子类
	}

	/**
	* 执行具体的操作行为,假设本处只执行EAT、SLEEP、WORK 3个行为
	* @param act  具体的行为对象
	*/
	public static void fun(Action act){
   
     
		act.command(Action.EAT);	// 调用“吃”操作
		act.command(Action.SLEEP);	// 调用“睡”操作
		act.command(Action.WORK);	// 调用“工作”操作
	}
}	

程序执行结果:

机器人充能!
机器人正在工作!
人类正在吃饭!
人类正在睡觉休息!
人为了生存在努力工作!
猪正在啃食槽!
猪在睡觉养膘!

此程序中的 fun() 方法实现了固定的行为操作,并且这些具体的行为都是根据传递的子类的不同而有所不同,由于机器人没有“”这个功能,所以方法体为空,表示此操作不起作用。

这些不同的类型最终都在行为上成功地进行了抽象,即如果要使用行为操作,就必须按照 Action 类的标准来实现子类。

2️⃣ 接口

利用抽象类可以实现对子类覆写方法的控制,但是抽象类的子类存在一个很大的问题——单继承局限,所以为了打破这个局限,就需要用 Java 中的接口来解决。同时在开发中为了将具体代码的实现细节对调用处进行隐藏,也可以利用接口来进行方法视图的描述。

2.1 接口定义

如果一个类只是由抽象方法和全局常量组成的,那么在这种情况下不会将其定义为一个抽象类,而通常只会将其定义为接口。所以所谓的接口严格来讲就属于一个特殊的类,而且这个类里面只有抽象方法与全局常量。

从Java发展之初,接口中的定义组成就是抽象方法与全局常量,但是这一概念在 JDK 1.8中被打破,接口中可以定义更多的成员,包括:default修饰的普通方法、static修饰的静态方法,但是考虑到实际开发的应用问题,还是建议先掌握接口最基础的定义形式 。

在Java 中可以使用 interface 关键字来实现接口的定义,下面来看具体的代码。

//	范例 11: 定义接口
interface A{
   
     		//定义接口
	public static final String MSG="XIAOSHAN"; 	//全局常量
	public abstract void print();            	//抽象方法
}

此程序定义了一个 A 接口,在此接口中定义了一个抽象方法 print() 和一个全局常量 MSG,由于接口中存在抽象方法,所以接口对象不可能直接使用关键字 new 进行实例化的操作。因此,接口具有以下使用原则。

  • 接口必须要有子类,一个子类可以使用 implements 关键字实现多个接口,避免单继承局限;
  • 接口的子类要么覆写接口中的全部抽象方法,要么也是一个抽象类;
  • 接口的对象可以利用子类对象的向上转型进行实例化操作。

对接口而言,其组成部分就是抽象方法和全局常量,所以很多时候为了省略编写,不写 abstractpublic static final,并且在方法上是否编写 public 结果都是一样的,因为在接口里面只能够使用一种访问权限——public,所以以下两个接口的定义最终效果就是完全相同的。

//	范例 12: 两种接口功能完全等价
interface A{
   
                                          
	public static final String MSG="XIAOSHAN";                   
	public abstract void fun();                                    
}

interface A{
   
     
	String MSG="HELLO";
	void fun();
}

即便在接口的方法中没有写 public,其最终的访问权限也是public,绝对不会是 default(默认)权限。所以为了准确定义,建议在接口定义方法时要写上 public,如下代码所示。

//	范例 13: 接口方法定义时建议加上public
interface A{
   
     
	String MSG="HELLO";
	public void fun();
}

在实际的开发中,只要是定义接口,大部分情况下都是以定义抽象方法为主,很少有接口只是单纯地去定义全局常量。

//	范例 14: 实现接口
interface A {
   
     		//定义接口
	public static final String MSG="HELLO";	//全局常量
	public abstract void print();			//抽象方法
}

interface B {
   
     		//定义接口
	public abstract void get();				//抽象方法
}

class X implements A,B{
   
                         	//X类实现了 A和B 两个接口
	public void print(){
   
                     	//覆写A接口的抽象方法
		System.out.println("A接口的抽象方法!");
	}
	public void get(){
   
                          	//覆写B 接口的抽象方法
		System.out.println("B接口的抽象方法!");
	}
}

public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		//此时X类是 A和B 两个接口的子类,所以此类对象可以同时实现两个接口的向上转型
		X x = new X();	//实例化子类对象
		A a = x;	//向上转型
		B b = x;	//向上转型
		a.print();	//调用被覆写过的方法
		b.get();	//调用被覆写过的方法
		System.out.println(A.MSG);	//直接访问全局常量
	}
}

程序执行结果:

A 接口的抽象方法!
B 接口的抽象方法!
HELLO

此程序定义了两个接口 AB, 同时在定义 X 子类时利用 implements 关键字实现了两个接口,这样在 X 子类中就必须覆写两个接口中提供的全部抽象方法,同时 X 类的对象也就可以利用向上转型的概念,为 AB 两个接口进行对象的实例化操作。

范例14的代码实例化了 X 类对象,由于 XAB 的子类,因此 X 类的对象可以变为A 接口或 B接口类的对象。

//	范例 15: 接口的转换
public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		A a = new X();	//对象向上转型
		B b = (B)a;		//a实际上代表的是X类对象
		b.get();		//调用B接口方法
		System.out.println(a instanceof A);		//判断a是否是A接口实例
		System.out.println(a instanceof B);		//判断a是否是B接口实例
	}
}

程序执行结果:

B接口的抽象方法!
true
true

此程序从定义结构上来讲,AB接口没有任何直接联系,但是这两个接口却同时拥有一个子类——X类,因为最终实例化的是X子类,而这个子类属于B类的对象。所以本程序的代码都可以成功编译并执行,只不过从代码的阅读上来讲,并不具备良好的结构。

如果一个子类既要继承抽象类又要实现接口,那么应该采用先继承 (extends) 后实现接口(implements) 的顺序完成。

//	范例 16: 子类继承抽象类并实现接口
interface A{
   
     	//定义接口
	public abstract void print();	//抽象方法
}

interface B{
   
     	//定义接口
	public abstract void get();		//抽象方法
}

abstract class C{
   
     	//定义抽象类
	public abstract void change();	//定义抽象方法
}

class X extends C implements A, B{
   
     	//X 类继承了抽象类C, 实现了A 和B 两个接口
	public void print(){
   
     		//覆写接口A 中的方法
		System.out.println("A接口的抽象方法!");
	}
	public void get(){
   
     			//覆写接口B 中的方法
		System.out.println("B接口的抽象方法!");
	}
	public void change(){
   
     		//覆写抽象类C 的方法
		System.out.println("C类的抽象方法!");
	}
}

此程序的X 子类是接口 AB 以及抽象类C 三个的子类,所以X 类的对象可以同时被三个父类引用所指向。

一个抽象类可以继承一个抽象类或者实现若干个接口,反过来,一个接口却不能继承抽象类。 但是一个接口却可以使用 extends 关键字同时继承多个父接口。

//	范例 17: 观察接口的多继承
interface  A{
   
     	//定义父接口
	public void funA();	
}

interface  B{
   
     	//定义父接口
	public void funB();
}

interface C extends A,B{
   
       // 利用extends,实现接口多继承
	public void funC();
}

class X implements C{
   
     	//实现C 接口子类要覆写全部抽象方法 
	public void funA(){
   
     }		//A 接口定义的方法
	public void funB(){
   
     }		//B 接口定义的方法
	public void funC(){
   
     }		//C 接口定义的方法
}

此程序在定义接口 C 时使用 extends 关键字继承了两个父接口,这就相当于C 接口中一共定义3个抽象方法 (funA()funB() 通过父接口继承下来),所以在定义 X 子类时必须覆写3个抽象方法。

需要明白的是,从继承关系上讲抽象类的限制要比接口多。
(1)一个抽象类只能继承一个抽象的父类,而接口没有这个限制, 一个接口可以继承多个父接口
(2)一个子类只能继承一个抽象类,却可以实现多个接口

所以,在整个Java中,接口主要用于解决单继承局限的问题。

虽然从接口本身的概念上来讲只能够由抽象方法和全局常量组成,但是所有的内部结构不受这些要求的限制,也就是说在接口里面可以定义普通内部类、抽象内部类、内部接口。

//	范例 18: 在接口里定义抽象类
interface A{
   
     
	public void funA();

	abstract class B {
   
     	//定义接口中的抽象类
		public abstract void funB();
	}
}

class X implements A{
   
     	//X 实现了A 接口
	public void funA(){
   
     
		System.out.println("Hello World!");
	}
	class Y extends B{
   
     	//内部抽象类的子类,可以选择性继承
		public void funB(){
   
     }
	}
}

此程序在A 接口的内部定义了一个内部抽象类B, 这样在A 接口的X 子类中就可以根据自己的需求来选择是否要继承内部的抽象类B

//	范例 19: 在一个接口内部如果使用static 去定义一个内部接口,该接口就表示是一个外部接口
interface A{
   
     
	public void funA();

	static interface B{
   
     	//外部接口
		public void funB();
	}
}

class X implements A.B(	//X 实现了A.B 接口
	public void funB(){
   
     }
}

此程序利用 static 定义了一个 A.B 的外部接口,这样子类可以直接实现 A.B 接口并覆写接口中的抽象方法。

2.2 接口的应用——标准

在日常生活中,人们经常会听到有关接口的术语,其中最常见的就是 USB 接口。使用 USB 接口可以连接各种标准设备,例如 U 盘、打印机、MP3 等,如下图所示。

*

通过上面的类图关系可以发现,计算机应该作为一个类,而计算机上要提供对 USB 接口标准的支持,这样不管什么设备,在计算机上都会按照 USB 接口中定义的标准执行,符合 USB 接口标准的可以有很多类设备。
如果要进行代码开发, 一定要首先开发出 USB 接口标准,因为有了标准后,计算机才可以去使用这些标准,设备厂商才可以设计USB接口设备。

//	范例 20: 定义 USB 标准
interface  USB{
   
     		//定义标准一定就是接口
	public void start();	//USB 设备开始工作
	public void stop();		//USB 设备停止工作
}

此程序定义的 USB 接口中只提供开始工作与停止工作两个操作方法。而现在假设只要有设备插入计算机,就自动调用 start()stop()两个方法。

//	范例 20: 定义计算机类
class Computer {
   
     
	public void plugin(USB usb){
   
      	// 插入USB 接口设备(子类对象)
		usb.start();	//开始工作
		usb.stop();		//停止工作
	}
}

在计算机类中提供有一个 plugin()方法,这个方法可以接收 USB 接口实例,这样不管有多少种 USB设备 (USB 接口对应子类) ,都可以插入在计算机上进行工作。下面依据 USB 接口标准定义出两个子类。

//	范例 20: 定义U盘子类
class Flash implements USB{
   
         // 实现 USB 接口
	public void start(){
   
     
		System.out.println("U盘开始使用");
	}
	public void stop(){
   
     
		System.out.println("U盘停止使用");
	}
}

//	范例 20: 定义打印机
class Print implements USB{
   
       //  定义打印机
	public void start(){
   
     
		System.out.println("打印机开始工作");
	}
	public void stop(){
   
     
		System.out.println("打印机停止工作");
	}
}

按照这样的方式,准备出几万个子类都可以,并且这几万个子类都可以在电脑的plugin()方法上使用。

//	范例 20: 测试代码
public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		Computer com = new Computer();	//实例化计算机类
		com.plugin(new Flash());		//插入USB 接口设备
		com.plugin(new Print());		//插入USB 接口设备
	}
}

程序执行结果:

U 盘开始使用
U 盘停止使用
打印机开始工作
打印机停止工作

此程序首先实例化了一个 Computer 计算机类的对象,然后就可以在计算机上插入USB 设备 (USB 接口子类)。

实际上在现实生活中也到处存在这样标准的定义,例如:酒店门口会明确标记“宠物不允许进入" 的警告牌;设计低于60脉的电动车不允许上主路。
所以,大家不应该把程序只当作程序来理解,程序源自于生活,要从现实生活的角度去理解程序,这样才不会被没有意义的问题困扰,这样才可以写出优秀的代码。

2.3 接口的应用——工厂设计模式 (Factory)

工厂设计模式,是Java 开发中使用的最多的一种设计模式,那么为什么叫做工厂设计模式?工厂设计模式又有哪些特征或作用呢?在详细解释问题之前,先观察以下程序。

//	范例 21: 观察程序代码问题
interface Fruit{
   
     		//定义接口
	public void eat();	//定义抽象方法
}

class Apple implements Fruit {
   
     	//定义接口子类
	public void eat(){
   
     	//覆写抽象方法
		System.out.println("吃苹果");
	}
}

public class TestDemo{
   
     
	public static void main(String args[]){
   
     
		Fruit f = new Apple();        		//子类实例化父类对象
		f.eat();                        	//调用被覆写过的方法
	}
}

程序执行结果:

吃苹果

此程序首先定义了一个表示水果的 Fruit 接口,然后为 Fruit 定义了一个苹果 (Apple) 子类,在主类中通过 Apple 类实例化 Fruit 接口对象,所以当利用 Fruit 接口对象调用 eat()方法时调用的是被子类覆写过的方法。

此程序是已经学习过的大家也熟知的程序结构,因为接口不能够被直接实例化对象,所以必须利用向上转型技术,通过子类实例化父接口对象,其关系如下图所示。但是这样的代码实现足够合理完善吗?

*

如果要想确认一个代码的编写风格是否良好,应该至少遵从以下两个标准。
(1)客户端(现在为主方法)调用简单,不需要关注具体的细节;
(2)程序代码的修改,不影响客户端的调用,即使用者可以不去关心代码是否变更。

根据以上两个标准,就可以发现此程序设计上的问题。程序在取得接口的实例化对象时明确地指明了要使用的子类"Fruit f = new Apple()",而关键的问题就出现在关键字 “new” 上。因为一个接口不可能只有一个子类,所以对于 Fruit 也有可能产生多个子类对象,而一旦要扩充子类,客户端中的使用也就有可能还会与新的子类有关系。

下面通过程序建立一个Orange 子类。

//	范例 21: 定义新的子类
class Orange implements Fruit {
   
                //定义接口子类
	public void eat(){
   
                                  //覆写抽象方法
		System.out.println("吃橘子");
	}
}

此程序的客户端上要想得到这个新的子类对象,则需要修改代码。

//	范例 21: 修改客户端代码
public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		Fruit f = new Orange();  / 子类实例化父类对象
		f.eat();                               //调用被覆写过的方法
	}
}

程序执行结果:

吃橘子

此程序客户端的代码更换了一个子类 Orange, 所以修改了客户端的代码,将 Apple 子类替换为 Orange 子类,如下图所示。

*

这个时候如果有更多的子类呢?难道每次都要去修改实例化接口的子类吗?在整个过程中,客户端关心的事情只有一件:如何可以取得 Fruit 接口对象。至于说这个对象是被哪个子类所实例化的客户端根本就不需要知道,所以在整个代码中最大的问题就在于关键字 “new” 的使用上。

那么该如何去解决这个关键字 new 所带来的耦合度问题呢? 大家一起回顾一下 JVM 的核心原理,在Java 中 的JVM 为了解决程序与操作系统的耦合问题,在程序与操作系统之间 加入了一个中间过渡层——JVM, 由 JVM 去匹配不同的操作系统,只要 JVM 的核心支持不变,程序就可以在任意的操作系统间进行移植。

所以解决办法就产生了,即想办法让客户端只看见接口而不让其看见子类,但是需要一个中间的工具类来取得接口对象,如下图所示。这样客户端就不再需要关心接口的子类,只要通过 Factory (工厂类) 程序类就可以取得接口对象。

*

至此为止已经为大家分析了关键字 new在直接实例化接口上所带来的问题。实际上这种耦合问题在很多项目开发中都会存在,很多开发者在面对这种耦合问题时往往会采用大量的结构设计进行回避,可是这样的代码维护成本太高了。所以在Java开发中有一个Spring框架,其核心目的就是解决这种代码耦合问题,后面会出专栏专门详细介绍这个框架,请大家持续关注。

//	范例 22: 增加一个工厂类进行过渡
class Factory  {
   
                    //定义工厂类,此类不提供属性

	/**
	*取得指定类型的接口对象
	*@param  className 要取得的类实例化对象标记
	*@return 如果指定标记存在,则 Fruit接口的实例化对象,否则返回null
	*/
	public static Fruit getInstance(String className){
   
     
		if ("apple".equals(className))(     //  是否是苹果类
			return new Apple();
		} else if ("orange".equals(className)){
   
     // 是否是橘子类
			return new Orange();
		}else{
   
     
			return null;
		}
	}
}

public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		Fruit f = Factory.getInstance("orange");  //通过工厂类取得指定标记的对象 
		f.eat();                                 	//调用接口方法
	}
}

程序执行结果:

吃橘子

此程序在客户端的操作上取消关键字 new 的使用,而使用 Factory.getInstance() 方法根据指定子类的标记取得接口实例化对象,这时客户端不再需要关注具体子类,也不需要关注 Factory 类是怎样处理的,只需要关注如何取得接口对象并且操作。这样的设计在开发中就称为工厂设计模式。

2.4 接口的应用——代理设计模式 (Proxy)

代理设计也是在 Java 开发中使用较多的一种设计模式,所谓代理设计就是指一个代理主题来操作真实主题,真实主题执行具体的业务操作,而代理主题负责其他相关业务的处理。就好比在生活中经常使用到的代理上网,客户通过网络代理连接网络,由代理服务器完成用户权限、访问限制等与上网操作相关的操作,如下图所示。

*

不管是代理操作也好,真实的操作也好,其共同的目的就是上网,所以用户关心的只是如何上网, 至于里面是如何操作的用户并不关心,因此可以得出下图所示的分析结果。

*

//	范例 23: 代理设计模式实现
interface Network{
   
     		//定义Network 接口
	public void browse();	//定义浏览的抽象方法
}

class Real implements Network{
   
     	//真实的上网操作
	public void browse(){
   
     		//覆写抽象方法
		System.out.println("上网浏览信息");
	}
}

class Proxy implements Network{
   
     	//代理上网
	private Network network;	//设置代理的真实操作
	
	public Proxy(Network network){
   
     	//设置代理的子类
		this.network = network;
	}

	public void check(){
   
     	//与具体上网相关的操作
		System.out.println("检查用户是否合法");
	}
	public void browse(){
   
     	
		this.check();		//可以同时调用多个与具体业务相关的操作
		this.network.browse();		//调用真实上网操作
	}
}

public class TestDemo{
   
     
	public static void main(String args[]){
   
     
		Network net = null;	//定义接口对象
		net = new Proxy(new Real());	//实例化代理,同时传入代理的真实操作
		net.browse();		//客户只关心上网浏览一个功能
	}
}

程序运行结果:

检查用户是否合法
上网浏览信息

在此程序中,真实主题实现类 (Real) 完成的只是上网的最基本功能,而代理主题 (Proxy) 要做比真实主题更多的相关业务操作。

3️⃣ 抽象类与接口的区别

通过以上介绍可以发现,抽象类和接口都会强制性地规定子类必须要覆写的方法,这样在使用形式上是很相似的,那么在实际开发中是使用抽象类还是使用接口呢?这取决于你的实际业务场景要求,而为了让大家更加清楚两个概念的异同,下面给出抽象类与接口的对比信息。

区别 抽象类 接口
关键字 abstract class interface
组成 构造方法、普通方法、抽象方法、static方法、常量、变量 抽象方法、全局常量。JDK8之后增加了default方法、static方法
子类使用 子类extends抽象类 子类 implements 接口,接口…
关系 抽象类可以实现多个接口 接口不能继承抽象类,可以继承多个父接口
权限 可以使用各种权限 只能使用 public权限
限制 单继承局限 没有单继承局限
设计目的 代码复用和简化设计,用于表示某个类的通用行为 一组规范,用于指导类的行为,以便多个类可以遵循同样的规范进行操作
子类 抽象类和接口都必须有子类,子类必须要覆写全部的抽象方法
实例化对象 依靠子类对象的向上转型进行对象的实例化

经过比较可以发现,抽象类中支持的功能绝对要比接口多,但是其有一点不好,那就是单继承局限,所以这重要的一点就掩盖了所有抽象类的优点,即当抽象类和接口都可以使用时,优先考虑接口。

对于实际的项目开发,可能会有各种各样的问题,为了方便大家快速使用接口的概 念,下面给出一些依据个人经验总结的参考意见:

  • 在进行某些公共操作时一定要定义接口;
  • 有了接口就需要利用子类完善方法;
  • 如果是自己写的接口,那么不要使用关键字new直接实例化接口子类,而应该使用工厂类完成实例化。

到此我们已经学习过 对象、类、抽象类、接口、继承、实现等,那么这些概念都属于什么样的关系呢?在开发中,又该如何使用这几个概念呢? 下面我们进行一个小结。

在Java中,对象、类、抽象类、接口、继承和实现是面向对象编程的基本概念,它们之间存在以下关系:

  • 对象:对象是类的一个实例,它具有特定的状态和行为。它可以使用类中定义的方法来改变其状态或执行特定的操作。
  • :类是一种用户自定义的数据类型,用于定义对象的属性和方法。它是对象的蓝图或模板,描述了对象的特征和行为。
  • 抽象类:抽象类是一种特殊的类,无法实例化。它可以包含抽象方法和非抽象方法。抽象方法只有声明而没有实现,子类必须实现这些抽象方法。抽象类提供了一种通用的实现或默认行为,供子类继承和实现。
  • 接口:接口是一种完全抽象的类,它只包含抽象方法和常量。接口定义了一组行为规范,可以被多个类实现。一个类可以实现一个或多个接口,并提供接口中定义的方法的具体实现。
  • 继承:继承是面向对象编程的重要特性之一。它允许一个类继承另一个类的属性和方法。通过继承,子类可以获得父类的属性和方法,并可以扩展或重写这些属性和方法。
  • 实现:实现是指一个类遵循接口的规范,提供接口中定义的所有方法的具体实现。通过实现接口,类可以满足接口的要求并实现特定的功能。

在开发中,你可以根据需求合理地使用这些概念:

  • 使用类来定义对象的属性和方法,并创建对象来调用这些方法和操作属性。
  • 当有一组类共享某些相似的属性和方法时,可以使用继承来减少代码的重复,并在子类中扩展或重写父类的行为。
  • 当需要定义一组相关但无法具体实现的方法时,可以使用抽象类或接口来声明这些方法,并由子类或实现类提供具体实现。
  • 如果接口仅定义了行为规范而不关心具体实现,可以将多个类实现该接口,从而实现多态性和解耦。
  • 在设计类关系时,需要考虑继承、实现和接口的合理使用,以便构建可维护、灵活和扩展的软件系统。

* 总结

抽象类和接口是面向对象编程中重要的概念,在Java开发中起着关键作用。通过本文,我们深入了解了抽象类和接口的定义、相关限制以及它们的应用场景。

抽象类是一种特殊的类,无法实例化,主要用于提供通用的实现或默认行为。它可以包含抽象方法和非抽象方法,并通过继承来扩展和重写这些方法。抽象类在模板设计模式中广泛应用,通过定义抽象类作为骨架,子类可以继承并实现具体的细节,从而实现代码复用和灵活性。

接口是一种完全抽象的类,只包含抽象方法和常量。接口定义了一组行为规范,可以被多个类实现。接口在标准制定和多态性方面扮演着重要角色。另外,工厂设计模式和代理设计模式都广泛使用了接口。工厂设计模式利用接口来创建对象的实例,提供了一种灵活的对象创建机制。代理设计模式通过接口实现和代理类的创建,实现了对目标对象的间接访问和控制。

在抽象类和接口之间,存在一些关键区别。首先,抽象类可以包含方法的具体实现,而接口只能定义方法的签名。其次,一个类可以继承一个抽象类,但可以实现多个接口。另外,抽象类适用于描述一种特殊类型的对象,而接口更适合描述一组相关行为的规范。


[* ]nbsp_nbsp 8