前言
在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