抽象类与接口
abstract类和abstract方法
abstract类
用关键字abstract修饰的类称为abstract类(抽象类),例如:
abstract class A {
...
}
用关键字abstract修饰的方法为abstract方法(抽象方法),如:
abstract int min(int x,int y);
注 对于abstract方法,只允许声明不允许实现(没有方法体),不允许final和abstract同时修饰一个方法或类,也不允许static修饰abstract方法,即abstract方法必须是实例方法。
abstract方法
-
abstract类中可以有abstract方法(abstract 类里也可以没有 abstract 方法。)
也可以有非abstract方法(但非abstract类中不可以有abstract方法)
下面的 A 类中的 min()方法是 abstract 方法, max()方法是普通方法(非 abstract 方法)。
abstract class A{ abstract int min(int x,int y); int max(int x,int y){ return x>y?x:y; } }
-
abstract类不能用new运算符创建对象
如果一个非抽象类是某个抽象类的子类,那么它必须重写父类的抽象方法(所有的抽象方法。子类也是抽象类可以不全部实现,可直接继承),给出方法体,即这是为什么不用final和abstract同时修饰一个方法或类的原因。
-
abstract类的对象作为上转型对象
可以使用 abstract 类声明对象, 尽管不能使用 new 运算符创建该对象, 但该对象可以成为其子类对象的上转型对象, 那么该对象就可以调用子类重写的方法。
(注 如果子类重写了父类的静态方法,那么子类对象的上转型对象不能调用子类重写的静态方法,只能调用父类的静态方法)
插点上转型对象的知识点
例子如下:
package com.test; abstract class GirlFriend { abstract void speak(); abstract void cooking(); } class ChinaGirlFriend extends GirlFriend { void speak(){ System.out.println("你好"); } void cooking(){ System.out.println("水煮鱼"); } } class AmericanGirlFriend extends GirlFriend { void speak(){ System.out.println("hello"); } void cooking(){ System.out.println("roast beef"); } } class Boy { GirlFriend friend; void setGirlfriend(GirlFriend f){ friend = f; } void showGirlFriend() { friend.speak(); friend.cooking(); } } public class Example5_12 { public static void main(String args[]) { GirlFriend girl = new ChinaGirlFriend(); //girl是上转型对象 Boy boy = new Boy(); boy.setGirlfriend(girl); boy.showGirlFriend(); girl = new AmericanGirlFriend(); //girl是上转型对象 boy.setGirlfriend(girl); boy.showGirlFriend(); } }
-
理解abstract类
- 抽象类可以抽象出具体的行为标准,该行为标准用抽象方法来表示。即抽象类封装了子类必须要有的行为标准。
- 抽象类声明的对象可以成为其子类的对象的上转型对象,调用子类重写的方法 ,即体现子类根据抽象类里的行为标准给出具体行为。
-
面向抽象编程
在设计程序时,经常会使用abstract类,其原因是,abstract类只关心操作,而不关心具体的操作实现的具体细节,将这些细节留给子类的设计者,这些子类给出具体的实例。设计一个程序时可以通过在abstract类中声明若干个abstract方法,表明这些方法在整个系统中的重要性,方法体的内容细节由它的非abstract子类去完成。
使用多态进行程序设计的核心技术之一是使用上转型对象,即将abstract类声明的对象作为其子类对象的上转型对象。
举例说明
创建个 Circle 类(圆类), 该类创建的对象 circle 调用 getArea()方法可以计算圆的面积
Circle.java
public class Circle{ double r; Circle(double r) { this.r=r; } public double getArea() { return(3.14*r*r); } }
实现一个Pillar类(柱类),该类调用getVolume()方法可以计算柱体体积
public class Pillar { Circle bottom; //bottom 是用具体类 Circle 声明的对象 double height; Pillar (Circle bottom,double height) { this.bottom=bottom; this.height=height; } public double getVolume() { return bottom.getArea()*height; } }
分析 上述Pillar类没什么不妥,但如果更改需求,比如求一个底为矩形的柱体体积,显然Pillar类无法实现,应为Pillar类面向的具体类。下面我们面向抽象重新设计此类,实现客户更改的需求,首先编一个抽象类Geometry,该抽象类定义了抽象方法getArea()。
Geometry.java
public abstract class Geometry { public abstract double getArea(); }
下面Pillar类的设计不在依赖具体的类,直接面向Geometry类,即 Pillar 类中的 bottom 是
用抽象类 Geometry 声明的对象,而不是具体类声明的对象。Pillar.java
public class Pillar { Geometry bottom; //bottom是抽象类Geometry声明的变量 double height; Pillar (Geometry bottom,double height) { this.bottom=bottom; this.height=height; } public double getVolume() { if(bottom==null) { System.out.println("没有底,无法计算体积"); return -1; } return bottom.getArea()*height; //bottom可以调用子类重写的getArea方法 } }
下面两个类都是Geometry的子类,两者必须重写Geometry的getArea()方法。
Circle.java
public class Circle extends Geometry { double r; Circle(double r) { this.r=r; } public double getArea() { return(3.14*r*r); } }
Rectangle.java
public class Rectangle extends Geometry { double a,b; Rectangle(double a,double b) { this.a=a; this.b=b; } public double getArea() { return a*b; } }
此后需求变化我们就不必更改Pillar类,只需在下列主类中来调用即可
Application.java
public class Application{ public static void main(String args[]){ Pillar pillar; Geometry bottom =null; pillar =new Pillar (bottom,100); //null底的柱体 System.out.println("体积"+pillar.getVolume()); bottom=new Rectangle(12,22); pillar =new Pillar (bottom,58); //pillar是具有矩形底的柱体 System.out.println("体积"+pillar.getVolume()); bottom=new Circle(10); pillar =new Pillar (bottom,58); //pillar是具有圆形底的柱体 System.out.println("体积"+pillar.getVolume()); } }
面向抽象编程的目的是为了应对用户需求的变化,将某个类中经常因需求变化而需要改动的代码从该类中分离出去。 面向抽象编程的核心是让类中每种可能的变化对应地交给抽象类的一个子类去负责,从而让该类的设计者不去关心具体实现, 避免所设计的类依赖于具体 的实现。 面向抽象编程使设计的类容易应对用户需求的变化。
开-闭原则
所谓 “开-闭原则" (Open-Closed Principle), 就是让设计的系统对扩展开放,对修改关闭。这句话的本质是指当系统中增加新的模块时,不需要修改现有的模块。在设计系统时,应当首先考虑到用户需求的变化,将应对用户变化的部分设计为对扩展开放,而设计的核心部分是经过精心考虑之后确定下来的基本结构,这部分应当是对修 改关闭的,即不能因为用户的祈求变化而再发生变化,因为这部分不是用来应对需求变化的。如果系统的设计遵守了 “开-闭原则“,那么这个系统一定是易维护的, 因为在系统中增加新的模块时,不必去修改系统中的核心模块。
上述pillar类满足开闭原则
Pillar类、Geometry类、Circle和Rectangle类看作是一个小的开发框架,将Application.java看作是使用该框架进行应用开发的用户程序,那么框架满足 “开-闭原则”,该框架相对用户的需求就比较容易维护, 因为当用户程序需要使用Pillar创建出具有三角形底的柱体时,系统只需简单地扩展框架,即在框架中增加一个Geometry的Triangle子类, 而无须修改框架中的其他类。
通常我们无法让设计的每个部分都遵守 “开-闭原则“,甚至不应当这样去做,应当把主要精
力用来集中应对设计中最有可能因需求变化而需要改变的地方, 然后想办法应用 “开-闭原则”。
举个例子
用类封装手机的基本属性和功能, 要求手机既可以使用移动公司的SIM卡, 也可以使用联通公司的SIM卡(可以使用任何公司提供的SIM卡)。
Application.java
public class Application {
public static void main(String args[]) {
MobileTelephone telephone = new MobileTelephone ();
SIM sim=new SIMOfChinaMobile();
sim.setNumber("13887656432");
telephone.useSIM(sim);
telephone.showMess();
sim=new SIMOfChinaUnicom();
sim.setNumber("13097656437");
telephone.useSIM(sim);
telephone.showMess();
}
}
MobileTelephone.java
public class MobileTelephone {
SIM card;
public void useSIM(SIM card) {
this.card=card;
}
public void showMess() {
System.out.println("使用的卡是:"+card.giveCorpName()+"提供的");
System.out.println("手机号码是:"+card.giveNumber());
}
}
SIM.java
public abstract class SIM {
public abstract void setNumber(String n);
public abstract String giveNumber();
public abstract String giveCorpName();
}
SIMOfChinaMobile.java
public class SIMOfChinaMobile extends SIM {
String number;
public void setNumber(String n) {
number = n;
}
public String giveNumber() {
return number;
}
public String giveCorpName() {
return "中国移动";
}
}
SIMOfChinaUnicom.java
public class SIMOfChinaUnicom extends SIM {
String number;
public void setNumber(String n) {
number = n;
}
public String giveNumber() {
return number;
}
public String giveCorpName() {
return "中国联通";
}
}
如果再增加一个 Java 源文件(对扩展开放), 该源文件有一个 SIM 的子类, 例如ChinaFeiTong 子类, 那么 MobileTelephone 类不需要做任何修改(对 MobileTelephone 类的修改关闭),应用程序中就可以让 telephone 对象使 用 ChinaFeiTong 类提供的 SIM 卡。满足开-闭原则。
接口与实现
接口定义
使用关键字interface定义接口。和类定义相似。分为接口声明和接口体
interface Printable{
final int MAX=100;
void add();
float sum(float x, float y);
}
接口声明
- interface 接口的名字
接口体
- 接口体中只有常量(没有变量)和抽象方法(没有普通方法)两部分。所有常量都是static常量,且访问权限一定都是public(允许省略public、final和static修饰符),所有抽象方法访问权限一定都是public(允许省略public abstract修饰符)
例如
interface Printable{
public static final int MAX= 100; //等价写法: l.吐 MAX = 100;
public abstract void add(); //等价写法: void add();
public abstract float sum(float x ,floaty);
}
实现接口
-
类实现接口中的方法,在类声明中使用implements关键字。实现多个接口用逗号隔开接口名。
class A implements Printable,Addable
再如,Animal的Dog子类实现Eatable和Sleepable接口
class Dog extends Animal implements Eatable,Sleepable
-
重写接口中的方法
如果非抽象类实现了某个接口,那么这个类必须重写这个接口中的所有方法(抽象类不用,可以重写也可以直接拥有(继承)接口中的方法),所有类在重写接口方法时候不仅要去掉abstract修饰符、给出方法体,且方法的权限一定要明显地用public来修饰,不然就降低了访问权限这不被允许。
-
接口细节说明
程序可以用接口名直接访问接口中的常量,实现了接口的类可以在类体中直接使用接口中的常量。定义接口时前面加public可以被任何类实现,不加public为友好接口可以被同一包中的类实现。
如果父类实现了某个接口,子类就自然实现了该接口,不用在显式地使用implements声明实现这个接口。
接口也可以被接口继承,使用关键字extends。由于接口中的方法和常量都是public,所以子接口继承父接口中全部的常量和方法。
接口的UML图
和类的UML图相似。分为三层。第一层名字层,第二层常量层,第三层方法层也称操作层。
接口回调
和类一样,接口也是 Java 中一种重要的数据类型,用接口声明的变量称作接口变量。接口属千引用型变量,接口变量中可以存放实现该接口的类的实例的引用, 即存放对象的引用。
在Java语言中,接口回调是指:可以把实现某 接口的类创建的对象的引用赋值给该接口声明的接口变量,那么该接口变量就可以调用被类实现的接口方法。 实际上, 当接口变矗调用被类实现的接口方法时,就是通知相应的对象调用这个方法。
注:接口回调非常类似于 上转型对象调用子类重写的方法 接口无法调用类中的其他非接口方法(这个和上转型对象也类似)。
例子
interface ShowMessage {
void 显示商标(String s);
}
class TV implements ShowMessage {
public void 显示商标(String s) {
System.out.println(s);
}
}
class PC implements ShowMessage {
public void 显示商标(String s) {
System.out.println(s);
}
}
public class Example6_2 {
public static void main(String args[]) {
ShowMessage sm; //声明接口变量
sm=new TV(); //接口变量中存放对象的引用
sm.显示商标("长城牌电视机"); //接口回调。
sm=new PC(); //接口变量中存放对象的引用
sm.显示商标("联想奔月5008PC机"); //接口回调
}
}
接口与多态
由接口产生的多态就是指不同的类在实现同一个接口时可能具有的实现方式,那么接口变量在回调接口方法就可能有多种形态。
例如下面 都是实现a和b的平均值
interface CompurerAverage {
public double average(double a,double b);
}
class A implements CompurerAverage {
public double average(double a,double b) {
double aver=0;
aver=(a+b)/2;
return aver;
}
}
class B implements CompurerAverage {
public double average(double a,double b) {
double aver=0;
aver=Math.sqrt(a*b);
return aver;
}
}
public class Example6_4 {
public static void main(String args[]) {
CompurerAverage computer;
double a=11.23,b=22.78;
computer = new A();
double result = computer.average(a,b);
System.out.printf("%5.2f和%5.2f的算术平均值:%5.2f\n",a,b,result);
computer = new B();
result= computer.average(a,b);
System.out.printf("%5.2f和%5.2f的几何平均值:%5.2f",a,b,result);
}
}
接口参数
如果一个方法的参数是接口类型,我们就可以将任何实现该接口的类的实例的引用传递给该接口参数,那么接口参数就可以回调类实现的接口方法。
interface SpeakHello {
void speakHello();
}
class Chinese implements SpeakHello {
public void speakHello() {
System.out.println("中国人习惯问候语:你好,吃饭了吗? ");
}
}
class English implements SpeakHello {
public void speakHello() {
System.out.println("英国人习惯问候语:你好,天气不错 ");
}
}
class KindHello {
public void lookHello(SpeakHello hello) { //接口类型参数
hello.speakHello(); //接口回调
}
}
public class Example6_5 {
public static void main(String args[]) {
KindHello kindHello=new KindHello();
kindHello.lookHello(new Chinese());
kindHello.lookHello(new English());
}
}
面向接口编程
利用接口也可以体现程序设计的 “开-闭原则",即对扩展开放, 对修改关闭。 例如, 程序的主要设计者可以设计出如图所示的一种结构关系。
从图 可以看出, 当程序再增加实现接口的类(由其他设计者去实现), 接口变扯variable所在的类不需要做任何修改, 就可以回调类重写的接口方法。
当然, 在程序设计好后, 首先应当对接口的修改 “关闭“,否则, 一旦修改接口, 例如,为它再增加一个abstract方法,那么实现该接口的类都需要做出修改。 但是, 程序设计好后, 应当对增加实现接口的类 ”开放“,即在程序中再增加实现接口的类时,不需要修改其他重要的类。
例子
设计 个广告牌,希望所设计的广告牌可以展示许多公司的广告词。
Advertisement.java
public interface Advertisement { //接口
public void showAdvertisement();
public String getCorpName();
}
AdvertisementBoard.java
public class AdvertisementBoard { //负责创建广告牌
public void show(Advertisement adver) {
System.out.println(adver.getCorpName()+"的广告词如下:");
adver.showAdvertisement(); //接口回调
}
}
BlackLandCorp.java
public class BlackLandCorp implements Advertisement {
public void showAdvertisement(){
System.out.println("**************");
System.out.printf("劳动是爹\n土地是妈\n");
System.out.println("**************");
}
public String getCorpName() {
return "黑土集团" ;
}
}
WhiteCloudCorp.java
public class WhiteCloudCorp implements Advertisement { //Avertisement接口
public void showAdvertisement(){
System.out.println("@@@@@@@@@@@@@@@@@@@@@@");
System.out.printf("飞机中的战斗机,哎yes!\n");
System.out.println("@@@@@@@@@@@@@@@@@@@@@@");
}
public String getCorpName() {
return "白云有限公司" ;
}
}
Example6_6.java
public class Example6_6 {
public static void main(String args[]) {
AdvertisementBoard board = new AdvertisementBoard();
board.show(new BlackLandCorp());
board.show(new WhiteCloudCorp());
}
}
abstract类和接口的比较
-
abstract类和接口都可以有abstract方法
-
接口中可以有常量,不能有变量;abstract类既可以有常量,也可以有变量
-
abstract类中也可以有非abstract方法,接口不可以。
在设计程序时应当根据具体的分析来确定是使用抽象类还是接口。abstract类除了提供重要的需要子类重写的abstract方法外,也提供了子类可以继承的变量和非abstract方法。 如果某个问题需要使用继承才能更好地解决, 例如, 子类除了需要重写父类的abstract方法, 还需要从父类继承一些变量或继承一些重要的非abstract方法,就可以考虑用abstract类。 如果某个问题不需要继承, 只是需要若干个 类给出某些重要的abstract方法的实现细节, 就可以 考虑使用接口。
{
public static void main(String args[]) {
AdvertisementBoard board = new AdvertisementBoard();
board.show(new BlackLandCorp());
board.show(new WhiteCloudCorp());
}
}
## abstract类和接口的比较
* abstract类和接口都可以有abstract方法
* 接口中可以有常量,不能有变量;abstract类既可以有常量,也可以有变量
* abstract类中也可以有非abstract方法,接口不可以。
在设计程序时应当根据具体的分析来确定是使用抽象类还是接口。abstract类除了提供重要的需要子类重写的abstract方法外,也提供了子类可以继承的变量和非abstract方法。 如果某个问题需要使用继承才能更好地解决, 例如, **子类除了需要重写父类的abstract方法, 还需要从父类继承一些变量或继承一些重要的非abstract方法,就可以考虑用abstract类**。 ***如果某个问题不需要继承, 只是需要若干个 类给出某些重要的abstract方法的实现细节, 就可以 考虑使用接口。***