前言
在Java中,接口和抽象類為我們提供了一種将類的對外接口與實作分離的更加結構化的方法。下面将介紹抽象類,它是普通的類與接口之間的一種中庸之道,接着再介紹接口。
抽象類和抽象方法
當我們僅是希望有一個基類可以提供統一的接口去控制它導出的所有子類,并且該基類沒有被執行個體化的必要時,我們就可以使用
抽象類
去建立這個基類。為了使抽象類不被執行個體化,我們就需要使用某種機制來限制。于是,Java中提供一種叫做
抽象方法
的機制(相當于C++中的純虛函數),這種方法是不完整的:僅有聲明而沒有方法體。
abstract void fun(); //抽象方法聲明文法
抽象類如果包含抽象方法,那麼抽象類就是不完整的,試圖産生該類的對象的時候,編譯器就會抛出錯誤資訊。
是以,我們就将包含抽象方法的類叫做抽象類。當然,如果一個類包含一個或者多個抽象方法,該類就必須被限定為抽象的。并且使用關鍵字
abstract
來限定。
需要注意,Java中的抽象類中除了包含抽象方法也可以包含具體的資料和具體的方法,但是為了程式的清晰度,抽象類中方法最好還是全是抽象方法。如果子類繼承自某一個抽象類,并且想建立子類的對象,那麼抽象類中的所有抽象方法在子類中都要被實作。否則,子類仍舊是一個抽象類,無法被執行個體化。
一個抽象類舉例:
public abstract Animal{
private String name;
public abstract void eat();
public String toString { return name;}
}
接口
如果說
abstract
關鍵字使得可以在類中建立一個或多個沒有方法體的方法給類提供了抽象的概念,那麼
interface
關鍵字就使得類的抽象更前一步。
使用interface關鍵字産生的類是一個完全抽象的類,其中的方法沒有任何具體實作。即,隻允許建立者确定類中的方法名、參數清單和傳回類型,但是沒有任何方法體。
一個類如果使用了某個接口那麼就必須得實作該接口中規定的所有方法,這倒像是“要幹什麼事,就必須遵守某種協定”一樣。
通常,為使一個方法可以在類間調用,兩個類都必須出現在編譯時間裡,以便Java編譯器可以檢查以確定方法特殊是相容的。這個需求導緻了一個靜态的不可擴充的類環境。在一個系統中不可避免會出現這種狀況:函數在類層次中越堆越高以緻該機制可以為越來越多的子類可用。
接口的設計避免了這個問題。它們把方法或方法系列的定義從類層次中分開。因為接口是在和類不同的層次中,與類繼承層次無關的類實作相同的接口是可行的。這是實作接口的真正原因所在,接口可以使代碼之間解除繼承限制,降低代碼耦合性。
接口的定義和實作
接口的建立同類建立一樣,隻需将class關鍵字替換為interface關鍵字,裡面的方法隻用聲明不用實作即可。接口中也可以包含域,但是這些域都是隐式地為static和final的。要讓某個類遵循某個接口(或者是一組接口)就需要使用
implements
關鍵字。
接口的定義
public interface USB{
String name = "USB";
public String getName();
}
通路權限修飾符可以為public也可以不寫。接口中的方法不能設定成private的,讓使用該接口的類不能夠實作該方法,是以接口中方法都是預設地為public。接口中的域是static和final的,是以定義時一定要初始化。
接口的實作
一旦接口被定義,一個或多個類便可以實作該接口。為實作該接口,在定義的類名後使用implement接接口名,然後在類中實作接口定義的方法。
可以使用接口引用指向實作了接口的類對象,就類似于使用基類的引用指向子類對象,于是就可以實作多态功能。
public class Mouse implements USB{
//實作接口中定義的方法
public String getName(){ return "Mouse USB";}
public static void main(String args[]){
USB usb = new Mouse(); //使用接口引用指向實作了接口的類對象
}
}
接口的局部實作
如果一個類不完全實作一個接口中的方法那麼該類就必須使用abstract修飾。也就是說,抽象類可以不完全實作接口中的方法。
完全解耦
隻要一個方法操縱的是類而非接口,那麼你就隻能在這個類或其子類上使用這個方法。即隻能操縱有繼承關系的類。如果你将此方法應用于非此繼承結構中的類,那麼就會出問題。
若這個方法是接口中的,那麼該方法便可以應用在實作了該接口的類對象上,不需要考慮類之間是否有繼承性。這樣就可以寫出複用性更好的代碼~
代碼說明(參考[1]):
class Processor{
public String name() {
return getClass().getSimpleName();
}
Object process(Object input) {
return input;
}
}
class Upcase extends Processor{
@Override
String process(Object input) {
return ((String)input).toUpperCase();
}
}
class DownCase extends Processor{
@Override
String process(Object input) {
return ((String)input).toLowerCase();
}
}
public class Apply {
//使用基類引用統一控制子類對象
public static void process(Processor p, Object s) {
System.out.println("Using Processor " + p.name());
System.out.println(p.process(s));
}
public static void main(String[] args) {
String s = "This Road Is Long.";
process(new Upcase(), s);
process(new DownCase(), s);
}
}
Apply.process()方法使用基類引用去同一控制對象。
在本例中,建立一個能夠根據所傳參數對象不同而具有不同行為方法,被稱為
政策設計模式
。這類方法包含所要執行的算法中不變的部分,而“政策”包含變化的部分。政策就是傳遞進去的參數對象,它包含要執行的代碼。這裡,Processor對象就是一個政策,在main()中有兩種不同類型的政策應用到了String類型的s對象上。
現在有一組電子濾波器,它們的代碼可能适用于Apply.process()方法。
class Waveform{
private static long counter;
private final long id = counter++;
public String toString() { return "Waveform:" + id;}
}
class Filter{
public String name() { return getClass().getSimpleName();}
public Waveform process(Waveform input) { return input;}
}
class LowPass extends Filter{
private double cutoff;
public LowPass(double cutoff) { this.cutoff = cutoff;}
@Override
public Waveform process(Waveform input) { return input;}
}
class HighPass extends Filter{
private double cutoff;
public HighPass(double cutoff) { this.cutoff = cutoff;}
@Override
public Waveform process(Waveform input) { return input;}
}
Filter和Processor具有相同的接口,但是因為Filter不是繼承自Processor的,是以不能将Filter應用于Apply.process()方法。Filter不能使用Apply.process()方法的主要原因在于:Apply.process()方法和Processor之間的耦合性過于緊密,導緻複用Apply.process()代碼時被禁止。
但是,如果将Processor換成是一個接口,那麼這些限制便會松動,也就可以複用Apply.process()方法。
public interface Processor{
String name();
Object process(Object input);
}
複用代碼的第一種方式就是用戶端程式員遵循接口來編寫類。
public abstract class StringProcessor implements Processor{
@Override
public String name() {
return getClass().getSimpleName();
}
public abstract String process(Object input);
public static void main(String[] args) {
String s = "This Road is Long.";
Apply.process(new Upcase(), s);
Apply.process(new Downcase(), s);
}
}
class Upcase extends StringProcessor{
@Override
public String process(Object input) { return ((String)input).toUpperCase(); }
}
class Downcase extends StringProcessor{
@Override
public String process(Object input) { return ((String)input).toLowerCase(); }
}
有時候就會遇見無法修改到類,在這種情況下,就可以使用
擴充卡設計模式
。擴充卡中的代碼将接受你所擁有的接口,并産生你所需要的接口。比如,修改電子濾波器使其可以使用Apply.process()。
class FilterAdapter implements Processor{
Filter filter;
public FilterAdapter(Filter filter) {
this.filter = filter;
}
public String name() { return filter.name();}
public Waveform process(Object input) {
return filter.process((Waveform)input);
}
}
public class FilterProcessor {
public static void main(String[] args) {
Waveform w = new Waveform();
Apply.process(new FilterAdapter(new LowPass(1.0)), w);
Apply.process(new FilterAdapter(new HighPass(2.0)), w);
}
}
/*
output:
Using Processor LowPass
Waveform:0
Using Processor HighPass
Waveform:0
*/
在這種使用擴充卡的方式中,FilterAdapter的構造器接受Filter的所有接口,然後生成需要的Processor接口對象。
将接口從具體實作中解耦使得接口可以應用于多種不同的具體實作,是以代碼也就更具可複用性。
Java中的多重繼承
接口是沒有任何具體實作的,即沒有任何與接口相關的存儲。是以,多個接口便可以組合使用。
使用具體類和多個接口的例子:
interface CanFight{
void fight();
}
interface CanSwim{
void swim();
}
interface CanFly{
void fly();
}
class ActionCharacter{
public void fight(); //與CanFight具有相同的方法特征簽名
}
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly{
public void swim(){}
public void fly(){}
}
public class Adventure{
public static void fi(CanFight x){ x.fight(); }
public static void sw(CanSwim x){ x.swim(); }
public static void fl(CanFly x){ x.fly();}
public static void ac(AcionCharacter x){ x.fight();}
public static void main(String args[]){
Hero h = new Hero();
fi(h);
sw(h);
fl(h);
ac(h);
}
}
前面說過一個類要使用一個接口就要實作該接口中的全部方法,但是很明顯Hero沒有顯式實作CanFight中的fight()方法。仔細觀察可以發現,Hero繼承的具體類ActionCharacter中有實作了的fight()方法。這樣,Hero也相當于實作了fight()方法。需要注意,繼承的具體類要寫在前面。
使用接口的核心原因
上面的例子展示了使用接口的核心原因:
- 為了能夠向上轉型為多個基類型;
- 第二個原因則是與抽象基類相同:防止用戶端程式員建立該類的對象,并確定這僅僅是一個接口。
使用繼承來擴充接口
我們可以通過繼承一個接口并添加新的方法聲明以生成新的接口,或者通過繼承多個接口以實作新的接口。這兩種方法都是擴充接口的主要方法。
interface Monster{
void menace();
}
//繼承并添加新的方法以生成新的接口
interface DangerousMonster extends Monster{
void destroy();
}
//繼承多個接口,組合成一個新接口
interface Lethal{
void kill();
}
interface Vampire extends DangerousMonster, Lethal{
void drinkBlood();
}
組合接口時的名字沖突
在前面多重繼承中,遇到CanFight和ActionCharacter都有一個相同的方法void fight(),但是這并沒有導緻什麼問題。
但是,如果在組合多個接口時出現兩個簽名不一樣或者傳回類型不同的方法時,會不會出現問題呢?
interface I1{ void f(); }
interface I2{ int f(int i);}
interface I3{ int f();}
class C { public int f(){ return 1;}}
class C2 implements I1, I2{
//兩個方法重載
public void f(){}
public int f(int i){ return i;}
}
class C3 extends C implements I2{
public int f(int i){ return i;} //重載
}
class C4 extends C implements I3{
//同CanFight和ActionCharacter一樣
}
//以下兩種方式不行!!!
//class C5 extends C implements I1{}
//interface I4 extends I1, I3{}
重載僅依靠傳回類型是無法區分的。在打算組合不同接口中使用相同的方法名通常會造成代碼可讀性的混亂,這是需要避免的。
接口與工廠
接口是實作多重繼承的途徑,而生成遵循某個接口的對象的典型方式就是
工廠方法設計模式
。與直接調用構造器不同,在工廠對象上調用的是建立方法,為該工廠對象将直接生成接口的某個實作的對象。理論上,通過這種方式,代碼将完全與接口的實作分離,這就使得我們可以透明地将某個實作替換為另一個實作。使用工廠模式的一個常見原因便是建立架構。
interface Service{
void method1();
void method2();
}
interface ServiceFactory{
Service getService();
}
class Implementation1 implements Service{
Implementation1(){}
public void method1(){System.out.println("Implementation1 method1");}
public void method2(){System.out.println("Implementation1 method1");}
}
class Implementation1Factory implements ServiceFactory{
public Service getService(){ return new Implementation1(); }
}
class Implementation2 implements Service{
Implementation2(){}
public void method1(){System.out.println("Implementation2 method1");}
public void method2(){System.out.println("Implementation2 method1");}
}
class Implementation2Factory implements ServiceFactory{
public Service getService(){ return new Implementation2(); }
}
public class Factories{
public static void serviceConsumer(ServiceFactory fact){
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String args[]){
serviceConsumer(new Implementation1Factory());
serviceConsumer(new Implementation2Factory());
}
}
/*
output:
Implementation1 method1
Implementation1 method1
Implementation2 method1
Implementation2 method1
*/
小結
Java中接口的最大意義就在于對外接口與實作分離,一個接口可以有不同的實作,減少了代碼中的耦合性。在本篇博文中還提到了三種設計模式:政策模式、擴充卡模式以及工廠模式,對三種設計模式介紹地比較簡單。在看《Java程式設計思想》時,也是首次學習,會存在不少疏忽之處,望各位看官指出。最後,在Java 8中,接口是有新的特性的,可以擁有方法實體,但是要聲明為default。對接口的介紹暫時到此,以後再繼續深入介紹。
參考:
[1] Eckel B. Java程式設計思想(第四版)[M]. 北京: 機械工業出版社, 2007
每天進步一點點,不要停止前進的腳步~