JAVA—抽象类和接口基础知识详解(及两者异同点)

       在本篇博客中将介绍JAVA里抽象类接口的基础知识以及两者的异同点,在有继承和多态的基础知识上学习会更好~

目录

抽象类基础知识

抽象类的定义、创建等基础

抽象类的几点说明(一) 

为何使用抽象方法

抽象类的几点说明(二)

接口基础知识

接口的定义、创建等基础

接口具体例题详析(重要)

关于接口的几点说明 

抽象类和接口的异同点

抽象类与接口的相同点

抽象类与接口的相异点(重要)

抽象类与接口的区别运用


抽象类基础知识

        在学习完继承和多态的知识后,我们对“类”的运用又有了更深的了解,那么在Java中还存在一个重要且特殊的类--抽象类,为什么说特殊,因为Java中的抽象类是不可以用于创建对象的,但是它可以包含抽象方法,这些方法将在具体的子类中实现,注意具体和子类两个关键字。下面是抽象类的具体定义:

        在继承的层次结构中,每个新的子类都使类变得更加明确和具体。如果从一个子类向父类追溯,类就会变得更通用,也就更加不明确。类的设计应该确保父类包含它的子类的共同特征。有时候,一个父类设计得非常抽象,以至于它都没有任何具体的实例,这样的类称为抽象类(abstract class) 这里要记住它的特殊性!没有任何实例!

抽象类的定义、创建等基础

        我们来举个栗子,创建下面这样一个GeometricObject (几何)类,这个在上一篇博客中也有,用来引入讲继承(大概意思是定义一个几何类,那么像Circle类和Rectangle类都可以从它衍生出来成为子类(并不是子集!))

public class GeometricObject  //创建这样一个试用于所有几何的大类,简称父类
{
	private String color = "white";        //一般设置为私有变量
	private boolean filled;               //布尔类型,是否填充
	private java.util.Date dateCreated;
	
	public GeometricObject()               //创建一个初始的或者是错误的几何对象,初始构造函数
	{
		dateCreated = new java.util.Date();//记录创建时间
	}
	
	public GeometricObject(String color,boolean filled) //创建一个初始的对象包含颜色和是否填充
	{
		dateCreated = new java.util.Date();  //更新创建时间
		this.color = color;
		this.filled = filled;
	}
	
	public String getColor()             //用户想得知颜色可以返回
	{
		return color;
    }
	
	public void setColor(String color)   //用户在外部更改颜色
	{
		this.color = color;
	}
	
	public boolean isFilled()
	{
		return filled;
	}
	
	public void setFilled(Boolean filled)
	{
		this.filled = filled;
	}
	
	public java.util.Date getDateCreated()
	{
		return dateCreated;
	}
	
//返回这个对象的基础信息,其实由前面学过的重载可知,我们这里是重载了toString这个函数,它的原始父类应该是在Object中
    @Override
	public String toString()              
	{
		return "created on " + dateCreated + "ncolor: " + color + " and filled: " + filled;
	}
}

        可以看到,GeometricObject类对几何对象的共同特征进行了建模,那么假设我们写了一个Circle类和Rectangle类(它的子类),然后它们都包含了分别用于计算圆和矩形的面积和周长的getArea()方法和getPerimeter()方法。

       因为可以计算所有几何对象的面积和周长,所以最好在GeometricObject类中定义getArea()和getPerimeter()方法。但是!这些方法不能在GeometricObject类中实现,因为它们的实现取决于几何对象的具体类型!这样的方法就称之为抽象方法(abstract method),在方法头中使用abstract修饰符表示,一旦在GeometricObject类中定义了这些方法后,GeometricObject就成为了一个抽象类,在类的头部使用abstract修饰符表示该类为抽象类。我们现在给出新的GeometricObject类的定义:(很重要!认真看!)

public abstract class GeometricObject  //抽象类前也要用abstract关键词定义
{
	private String color = "white";
	private boolean filled;
	private java.util.Date dateCreated;
	
	//创建一个初始的未设置的几何对象/构造方法
	protected GeometricObject()              //抽象类的构造方法定义为protected,因为它只被子类使用
	{ 
		dateCreated = new java.util.Date();
	}
	
	//创建一个初始的带设置的几何对象/这个也是构造方法,看方法头!
	protected GeometricObject(String color, boolean filled)
	{
		dateCreated = new java.util.Date();
		this.color = color;
		this.filled = filled;
	}
	
	//接下来颜色和是否填充的获取器和修改器方法和之前的都一样
	//下面这些就是抽象方法!也是使得这个类成为抽象类的原因!
	
	public abstract double getArea();
	public abstract double getPerimeter();
	//注意!这里都不需要写怎么计算或者返回什么,因为并不确定具体的几何对象,也就是具体的子类!
}

抽象类的几点说明(一) 

其实上面的注释中也提到了关于抽象类的重要知识点,我们在这里再总结一下:

1.抽象类和常规类很像,但是不能使用new操作符创建它的实例!抽象方法只有定义而没有实现!它的实现由子类提供!一个包含抽象方法的类必须声明为抽象类

2.抽象类的构造方法定义为protected,因为它只被子类使用!创建一个具体子类的实例时,其父类的构造方法被调用以初始化父类中定义的数据域!

那么Circle类在上一篇博客的继承中也有,这时候继承这个新定义的GeometricObject类就可以调试试试,写法上是没有任何差别的,调用定义抽象方法即可,同学们可以回上一篇看看

为何使用抽象方法

其实既然提到子类既然和之前的创建书写都没有什么不一样,那么为什么要在GeometricObject类中定义方法getArea()和getPerimeter()为抽象的,而不是在每个子类中定义他们,这样有什么好处?

我们来看一下下面这个代码,程序创建了两个几何对象:一个圆和一个矩形,调用equalArea方法来检查它们的面积是否相同,然后调用displayGeometricObject方法来显示它们:

public class TestGeometricObject
{
	public static void main(String[] args)
	{
		GeometricObject geoObject1 = new Circle(5);         //向上转型!
		GeometricObject geoObject2 = new Rectangle(5, 3);  //向上转型!
		
		System.out.println("The two objects hava the same area? " + equalArea(geoObject1, geoObject2));
		
		displayGeometricObject(geoObject1);
		displayGeometricObject(geoObject2);
	}
	
	public static boolean equalArea(GeometricObject object1, GeometricObject object2)
	{
		return object1.getArea() == object2.getArea();
	}

    public static void displayGeometricObject(GeometricObject object)
    {
	    System.out.println();
	    System.out.println("The area is " + object.getArea());
	    System.out.println("The perimeter is " + object.getPerimeter());
    }
}

//输出结果是:
//The two objects have the same area? false
//The area is 78.53981633974483
//The perimeter is 31.41592653589793
//The area is 13.0
//The perimeter is 16.0

 要注意的是:Circle类和Rectangle类中重写了定义在GeometricObject类中的getArea()和fetPerimeter()方法,从语句第五到六行可以看出,创建了一个新的圆和一个新的矩形,并把它们赋值给变量geoObject1和geoObject2,这两个变量都是GeometricObject类型的,这是向上转型!!!

我们这里分析一下,当调用equalArea方法时,由于geoObject1是一个圆,所以object1.getArea()使用的是Circle类定义的getArea()方法,而另一个是一个矩形,所以它调用时使用的是Rectangle类中定义的getArea方法,类似地在调用displayGeometricObject方法时,调用的那些对象方法都是子类中定义的同名的重写方法,这也表明,JVM在运行时根据调用该方法的实际对象的类型来动态地决定调用哪一个方法  (可以回顾多态的相关知识)

注意!如果GeometricObject类中没有定义getArea()等这些抽象方法,就不能再该程序中定义equalArea方法来计算这两个几何对象的面积是否相同,所以这可以看做是定义抽象方法的好处

抽象类的几点说明(二)

下面是关于抽象类值得注意的几点:

1.抽象方法不能包含在非抽象类中。如果抽象父类的子类不能实现所有的,注意是所有的抽象方法,那么子类也必须定义为抽象的。换句话说,在继承自抽象类的非抽象子类中,必须实现所有的抽象方法,还要注意到,抽象方法是非静态的!

2.象类不能使用new操作符来初始化,也就是说它没有实例,但是!仍然可以定义它的构造方法!这个构造方法在它的子类的构造方法中调用,比如我们上面的GeometricObject类的构造方法在Circle类和Rectangle类中调用,也就是回顾多态的向上转型知识!

3.包含抽象方法的类必须是抽象的。然而,可以定义一个不包含抽象方法的抽象类,这个抽象类用于作为定义新子类的基类。

4.子类可以重写父类的方法并将它定义为抽象的。这很少见,但是它在当父类的方法实现在子类中变得无效时是很有用的,那么也可以知道在这种情况下,这个子类也必须被定义为抽象的。

5.即是子类的父类是具体的,但是这个子类也可以是抽象的!例如,大类基类Object是具体的,但是它的子类比如我们定义的GeometricObject可以是抽象的。

6.不能使用new操作符从一个抽象类创建一个实例,但是抽象类可以用作一种数据类型。比如下面的语句创建一个元素是GeometricObjecy类型的数组是正确的:

GeometricObject[] objects = new GeometricObject[10];

然后可以创建一个GeomettricObject的实例,并将它的引用赋值给数组,如下:

object[0] = new Circle();

建议这里可以回顾多态的知识~

接口基础知识

        我们现在学习下接口的基础知识,其实接口在许多方面都与抽象类很相似,它也是一种与类相似的结构,用于为对象定义共同的操作。但是!它的目的是指明相关或者不相关类的对象的共同行为。例如,使用适当的接口,可以指明这些对象是课比较的、可食用的或者可克隆的,我们下面会深入理解这些话的意思。

接口的定义、创建等基础

       为了区分接口和类(我们上面也说了接口只是一种与类相似的结构),Java用下面的语法来定义接口:

modifier /*接口类别,公用的还是私有的等等*/ interface InterfaceName /*接口名称*/
{
	//Constant declarations  常数声明
	//Abstract method signatures  抽象方法签名
}

//interface为接口关键字,同抽象类的abstract一样
//下面是一个接口的例子:

public interface Edlib
{
	// Describle hou to eat
	public abstract String howToEat();   //必须包含抽象方法
}

       从上述定义代码也可以看出,接口包括常量和抽象方法(只能包括抽象方法,下面有总结),但是这里提一点,Cloneable接口是一个特殊情况,这个接口是空的,实现Cloneable接口的类标记为可克隆的,而且它的对象可以使用在Object类中定义的clone()方法克隆,感兴趣的同学可以去了解一下。

       在Java中,接口被看做是一种特殊的类。就像常规类一样,每个接口都被编译为独立的字节码文件。使用接口或多或少有点像使用抽象类,例如,可以使用接口作为引用变量的数据类型或类型转换的结果等。与抽象类相同的是,不能使用new操作符创建接口的实例!!我们也可以理解为因为接口中是包含抽象方法的!它需要依靠具体的类实现!!

接口具体例题详析(重要)

现在通过下面的这个包含接口实现(接口用上面的那个Edlib)、抽象类、常规类以及测试等等的代码例题来理解接口的运用与意义:

abstract class Animal            //由上节学习过的知识可知,我们创建了一个抽象类(必包含抽象方法)
{
	private double weight;       //和常规类一样,私有成员变量、修改器访问器等方法
	
	public double getWeight()
	{
		return weight;
	}
	
	public void setWeight(double weight)
	{
		this.weight = weight;
	}
	
	public abstract String sound();  //包含的抽象方法,返回动物的声音的方法,其依赖于具体的子类(哪种动物)实现
}

//使用Edlib接口来指定一个对象是否是可食用的,使用implements关键字让对象所属的类实现这个接口
class Chicken extends Animal implements Edible 
{
	@Override                        //方法重写前的关键字,可得知我们这里重写了接口中的抽象方法,这是否是必须的?
	public String howToEat()         //若不重写它或者说不需要它,你对这个子类使用接口的意义又何在?
	{
		return "Chicken: Fry it";
	}
	
	@Override
	public String sound()
	{
		return "Chicken: cock-a-doodle-doo";
	}
}

//这个Tiger类就仅仅是继承自Animal类了,并没有使用Edlib接口
class Tiger extends Animal
{
	@Override
	public String sound()           //从上节抽象类知识可知,继承自抽象类的子类必须要重写所有的抽象方法!
	{
		return "Tigher: RROOAARR";
	}
}

//注意!我们上面是继承自抽象类Animal的子类Chicken去和接口连接
//而在这里Fruit类为什么要被定义为abstract的?还是说它就是一个抽象类连接接口?
abstract class Fruit implements Edlib
{
	//创建一些数据域,访问器和修改器方法
}

class Apple extends Fruit
{
	@Override
	public String howToEat()
	{
		return "Apple: Make apple cider";
	}
}

class Orange extends Fruit
{
	@Override
	public String howToEat()
	{
		return "Orange: Make orange juice";
	}
}
//很显然,继承自Fruit的子类Apple和Orange有了“重写howToEat的资格”

//现在测试一下这些类和接口,观察输出结果
public class TestEdible 
{
	public static void main(String[] args)
	{
		//利用ArrayList来创建这些对象,更加方便,这是向上转型!最后的执行方法取决于对象的实际类型!可以回顾多态知识!
		Object[] objects = {new Tiger(), new Chicken(), new Apple()};
		
		//循环测试,注意判别条件
		for (int i = 0;i < objects.length;i++)
		{
			if (objects[i] instanceof Edible)                          //这里使用instanceof关键字来判断从属关系
				System.out.println(((Edible)objects[i]).howToEat());   //这种格式是怎样的?
			
			if (objects[i] instanceof Animal)
				System.out.println(((Animal)objects[i]).sound());
		}
	}
}

//输出的结果是:
//Tiger: RROOAARR              它没有Edlib接口,不输出howToEat,它属于Animal的子类,输出声音
//Chiken: Fry it               它有Edlib接口,输出howToEat
//Chicken: cock-a-doodle-doo   它属于Animal的子类,输出声音
//Apple: Make apple cider      它有Edlib接口,输出howToEat,它不属于Animal的子类,不输出声音

 这个例子使用了多个类和接口,通过下面的UML图来看看

关于接口的几点说明 

仔细看一遍代码和注解后,其实我们很容易理解捋清这些类的继承、接口关系等,捋清了这些后,我们总结一下在该例题中出现的知识点和需要注意的地方:

1.可以使用Edlib接口来指定一个对象是否是可食用的,这需要使用implements关键字让对象所属的类实现这个接口,比如我们这里的Chicken类和Fruit类实现了Edible接口

2.类和接口之间的关系称为接口继承,因为接口继承和类继承本质上是相同的,所以我们将它们都简称为继承,像实现多态的条件之一就是必须有继承关系,这里的继承包括接口继承

3.注意!当一个类实现接口时,该类实现定义在接口中的所有方法(不管是什么方法),在这里,Fruit类实现了Edible,但是!因为它没有实现howToEat方法,所以Fruit类必须定义为abstract,这就是原因!正因如此,Fruit的具体子类必须实现howToEat方法,理解!不然他们也要被定义为抽象类!

强调:类与类之间也好,类与接口之间也罢,这些继承、多态的关系一定要搞清楚,比如一个类是可以随意定义的还是必须有某种关键字的,这样我们自己才不会混乱,也不至于出错

4.接口中的所有的数据域都是public static final 而且所有的方法都是public abstract(这也是接口区别于抽象类的关键之一!抽象类只需包含抽象方法即可,其他类型的方法是允许有的),所以Java允许忽略这些修饰符,比如下列在接口中的定义都是等价的:

public static final int k = 1;    public abstract void p();      等价于  int k = 1;   void p();

抽象类和接口的异同点

抽象类与接口的相同点

        其实在刚开始学习接口的过程中,就有一直提到接口其实和抽象类在使用上是比较相似的,这里总结几点:

1.它们都是用来指定多个对象的共同特征的,即都是Java提供的对现实世界中的实体进行抽象的两种机制

2.抽象类与接口都不能被实例化,即不能使用new操作符来创建它们的实例

3.在抽象类与接口中所列的所有抽象方法(当然接口中只含有抽象方法)都必须在别的地方中被重写,如果达不到所有或者说没有被重写,那么它必须也定义为抽象的(例题中有说)

4.接口与类都可以定义一种类型。一个接口类型的变量可以引用任何实现该接口类的实例。如果一个类实现了一个接口,那么这个接口就类似于该类的一个父类,这也就是为什么叫做接口继承的原因。也就是说可以将接口当做一种数据类型使用,将接口类型的变量转换为它的子类,反过来也可以

       其实学习了多态的相关知识(上一篇博客中),这句话很容易理解,我们也可以结合下面这个图掌握。假设c是图中Class2的一个实例,那么c也是Object、Class1、Interface1、Interface1_1、Interface1_2、Interface2_1和Interface2_2的实例

       以上这些及上面的抽象类与接口的知识点中都是两者极大的相似性,通过上面Edible接口的例题中也可以看到(我有在例题中做注释),过多的相同点这里就不再赘述

抽象类与接口的相异点(重要)

虽说两者相同处众多,但其实在上面的学习过程里提及到的很多要点中我们也能感受到两者在语法和使用意义上的多处不同

从根本上来说,抽象类是类,而接口不是(当然我们时常也将它当成一种特殊的类),它的目的是为了给其他类提供行为说明,包括抽象类,比如上面例题中的 abstract class Fruit implements Edible 就是很好地说明这一点(这里是因为没有重写Edible中的抽象方法而被定义为抽象类),很多地方也可以看出接口与类的不同,比如类的关键字是class,而接口的是interface,接口中的方法只有方法头加分号结尾(因为都是抽象方法),没有方法主体,而类中的不是......

从语法层面来说两者也有以下不同:

 更详细一点可以总结为:

       上面两张表格就很详尽地说明了抽象类与接口的从多个方面的不同,接口的语法从变量方法的类别等很多方面都比抽象类严格得多,这些也强调过很多遍了,现在我们着重看一下两者在继承方面的区别

        一个类可以实现多个接口,但是只能继承一个父类(比如抽象类),也就是说我们在学继承关系的时候也提到,Java只允许为类的继承做单一继承,但是允许使用接口做多重继承!这样就解决了只能单一继承的弊端,却又没有破坏单一继承,这就是接口的好处!

        当然了,我们会利用extends关键字使某类继承某类,我们同样也可以使用extends关键字使接口继承其他接口,接口与接口之间正同于类与类之间存在继承关系的,这样的接口称为子接口 我们仔捋一捋类与类,接口与接口,类与接口之间的继承关系:

1.类与类之间可以存在继承,但是只能做单一继承,即一个类只能继承自一个父类,而一个父类可以有多个继承它的子类

2.接口与接口之间可以存在继承,且可以做多重继承,即一个接口可以继承自其他几个接口,比如:

public interface NewInterFace extends Interface1,...,InterfaceN {

    //constants and abstract methods   }

在这里,NewInterFace是Interface1,...,InterfaceN的子接口,一个实现NewInterFace的类必须实现在NewInterFace,Interface1,...,InterfaceN中定义的抽象方法(注意细节!包括NewInterFace)

3.一个类可以实现(继承)一个或多个接口,使用implements关键字(说得更准确一点是使用该关键字让这个类实现这个接口),称为接口继承。但是!接口不能继承类,只能继承接口!

4.我们都知道所有的类都共享一个根类Object,但是!接口没有共同的根!

抽象类与接口的区别运用

最后,其实在大多数情况下使用接口会比使用类更加地灵活,也更加推荐使用接口

因为接口可以为不相关类定义共同的父类型,这也是我们介绍接口时就说了它的目的:指明相关或者不相关类的对象的共同行为。而抽象类定义的抽象行为是为具有相关类而设置的

我们可以继续上面那个Edible接口的例子来更加清楚使用抽象类和使用接口的区别,现在考虑Animal类,假设Animal类中定义了howToEat方法:

abstract class Animal
{
	public abstract String howToEat();
}

//Animal的两个子类定义如下:
class Chicken extends Animal
{
	@Override
	public String howToEat()
	{
		return "Fry it";
	}
}

class Duck extends Animal
{
	@Override
	public String howToEat()
	{
		return "Roast it";
	}
}

假设给定这个继承体系结构,多态使你可以在一个类型为Animal的变量中保存Chicken对象或Duck对象的引用,(多态--向上转型)如:

public static void main(String[] args)
{
	Animal animal = new Chicken();
	eat(animal);
	
	animal = new Duck();    //重新赋值新的对象,这个知识点讲过很多了!
	eat(animal);
}

public static void eat(Animal animal)
{
	Ststem.out.println(animal.howToEat());
}

同样的又是一句说过很多很多遍的话:

JVM会基于调用方法时所用的确切对象来动态地决定调用哪个howToEat方法(回顾多态的知识)

       可以定义Animal的一个子类,但这里有个限制条件,那就是该子类必须是另一种动物,但是新问题产生了:如果一种动物不可食用,那么它继承Animal类就不合适了,因为我们在这里定义的抽象类就是Animal类,它的子类必须重写它的所有方法,这个很容易理解。

       而接口完全没有这种问题,接口比类拥有更多的灵活性,因为不用使所有东西都属于同一个类型的类,可以在接口中定义howToEat()方法,然后把它当做其他类的共同父类型。

/* 这个代码就像我们写在接口的那个一样(这里就不写了)(∩_∩) */

       要表示一个可食用对象的类,只需让该类实现Edible接口即可。顺理成章地,这个类就成了Edible类型的子类型,任何Edible对象都可以被传递以调用howToEat方法,我的意思是通过向上转型定义一个类型为Edible的变量去引用对象

        到此本篇的内容就全部结束了,逐一学懂抽象类接口(当然如果继承和多态的知识掌握得很好那学起来就相当容易),每个总结都写在文章里了,这样区别两者的异同也会轻车熟路,很深刻不会搞混,同的到底是在哪些地方同,不同的地方还是非常明显,一用起来就知道了,所以还是要多实践多写代码,加油!!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>