Java基础 -- 抽象类和接口
抽象类和接口都是用来为子类提供统一的接口,或者说是共同部分。
一 抽象类
Java提供了一种叫做抽象方法的机制,这种方法只有声明而没有方法体。下面是抽象方法声明所采用的语句:
abstract void f();
包含抽象方法的类叫做抽象类,如果一个类包含一个或多个抽象方法,该类就是抽象类,抽象类要使用abstract修饰。
如果直接使用抽象类创建一个对象,则编译会发生错误;如果从一个抽象类继承,并想创建该新类的对象,那么就必须为抽象类中的所有抽象方法提供方法定义,如果不这么做,那么子类也是抽象类,且编译器会强制使用abstract关键字来限定这个类。
我们也可以创建一个没有抽象方法的抽象类。考虑这种情况:如果有一个类,让其包含任何abstract方法都显得没有实际意义,而且我们也想要阻止产生这个类的任何对象,那么这时这么做就很有用了。
下面演示一个例子:
//抽象类 abstract class Instrument{ private int i; //抽象方法 public abstract void play(); public String what() {return "Instrument";} public abstract void adjust(); } class Wind extends Instrument{ public void play() { System.out.println("Wind.play()"); } public String what() {return "Wind";} public void adjust() {} } class Percussion extends Instrument{ public void play() { System.out.println("Percussion.play()"); } public String what() {return "Percussion";} public void adjust() {} } class Stringed extends Instrument{ public void play() { System.out.println("Stringed.play()"); } public String what() {return "Stringed";} public void adjust() {} } class Brass extends Instrument{ public void play() { System.out.println("Brass.play()"); } public String what() {return "Brass";} public void adjust() {} } class WoodWind extends Wind{ public void play() { System.out.println("WoodWind.play()"); } public String what() {return "WoodWind";} public void adjust() {} } public class Music { static void tune(Instrument i) { i.play(); } public static void main(String[] args) { Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), new Brass(), new WoodWind() }; for(int i=0;i<orchestra.length;i++) { tune(orchestra[i]); } } }
运行结果如下:
Wind.play() Percussion.play() Stringed.play() Brass.play() WoodWind.play()
二 接口
Interface关键字使抽象的概念更向前迈进了一步,abstract关键字允许人们在类中创建一个或多个没有任何定义的方法 -- 提供了接口部分,但是没有提供任何相应的具体实现,这些实现都是由此类的继承者来实现。
interface不仅仅是一个极度抽象的类,并且它还允许创建一个能够被向上转换为多种父类的类型,来实现某种类似多重继承变种的特性。
要创建一个接口,需要用Interface关键字来替换class关键字。就像类一样,可以在interface关键字之前使用public进行修饰(但是仅限于该接口在与其同名的文件中被定义)。如果不使用public关键字,则它默认是包访问权限,这样它就只能在一个包内可用。接口中方法都是public的(即使不显示声明)。接口也可以包含字段,但是这些字段隐式的是static和final的。
要让某一个类遵循某个特定接口(或者一组接口),需要使用implements关键字,它和继承有些类似。
下面演示一个例子:
//接口 interface Instrument{ //static和final 编译时常量 int VALUE=5; //只能是public(默认) void play(); void adjust(); } class Wind implements Instrument{ public void play() { System.out.println("Wind.play()"); } public String what() {return "Wind";} public void adjust() {} } class Percussion implements Instrument{ public void play() { System.out.println("Percussion.play()"); } public String what() {return "Percussion";} public void adjust() {} } class Stringed implements Instrument{ public void play() { System.out.println("Stringed.play()"); } public String toString() {return "Stringed";} public void adjust() {} } class Brass implements Instrument{ public void play() { System.out.println("Brass.play()"); } public String toString() {return "Brass";} public void adjust() {} } class WoodWind extends Wind{ public void play() { System.out.println("WoodWind.play()"); } public String toString() {return "WoodWind";} public void adjust() {} } public class Music { static void tune(Instrument i) { i.play(); } public static void main(String[] args) { Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), new Brass(), new WoodWind() }; for(int i=0;i<orchestra.length;i++) { tune(orchestra[i]); } } }
运行结果:
Wind.play() Percussion.play() Stringed.play() Brass.play() WoodWind.play()
三 完全解耦
如果有一个方法操作的是类而非接口(或者说一个方法需要传入一个类的对象引用为参数),那么你就只能使用这个类及其子类,如果你想要将这个方法作用于不再此继承结构中的某个类,那么编译器将会报错。接口在很大程度上可以放宽这种限制,因此,它使得我们可以编写可复用性更好的代码。
下面来看一个例子,假设有一个Processor类,它由一个name()方法,还有一个process()方法,该方法接收输入参数,修改它的值,然后输出。这个类别作为父类进行扩展,产生各种不同类型的Processor,在本例中,Processor的子类将作用于String对象上。
import java.util.Arrays; //父类 用于Object对象进行处理 class Processor{ public String name() { return getClass().getSimpleName(); } Object process(Object input) {return input;} } //子类 Object转换为大写字符串 class Upcase extends Processor{ String process(Object input) { return ((String)input).toUpperCase(); } } //子类 Object转换为小写字符串 class Downcase extends Processor{ String process(Object input) { return ((String)input).toLowerCase(); } } //子类 Object转换为数组 class Splitter extends Processor{ String process(Object input) { return Arrays.toString(((String)input).split(" ")); } } public class Apply { //该方法作用于Processor类及其子类 public static void process(Processor p,Object s) { System.out.println("Using processor " + p.name()); System.out.println(p.process(s)); } public static String s = "Disagreement with beliefs is my definition incorrect"; public static void main(String[] args) { process(new Upcase(),s); process(new Downcase(),s); process(new Splitter(),s); } }
输出结果为:
Using processor Upcase DISAGREEMENT WITH BELIEFS IS MY DEFINITION INCORRECT Using processor Downcase disagreement with beliefs is my definition incorrect Using processor Splitter [Disagreement, with, beliefs, is, my, definition, incorrect]
Apply.process()方法可以接受任何类型的Processor,并将其应用到一个Object对象上,然后输出结果。在上面这个例子中,创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。这类方法包含所要指向的算法中固定不变的部分,而”策略“包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。这里Processor对象就是一个策略,在main()中可以看到有三种不同的类型应用到了String类型的s对象上。
下面我们再来看一个例子,现在假设我们发现了一组电子滤波器,它们看起来好像适用于Apply.process()方法(即这组电子滤波器和process()的参数Processor具有相同的接口)。
class Waveform{ public static long counter; private final long id = counter++; public String toString() {return "Waveform "+ id;} } public class Filter { public String name() { return getClass().getSimpleName(); } Waveform process(Waveform input) {return input;} } class LowPass extends Filter{ double cutoff; public LowPass(double cutoff) {this.cutoff = cutoff;} public Waveform process(Waveform input) { return input; } } class HighPass extends Filter{ double cutoff; public HighPass(double cutoff) {this.cutoff = cutoff;} public Waveform process(Waveform input) { return input; } } class BandPass extends Filter{ double lowCutoff,highCutoff; public BandPass(double lowCutoff,double highCutoff) { this.lowCutoff = lowCutoff; this.highCutoff = highCutoff; } public Waveform process(Waveform input) { return input; } }
Filter和Processor具有相同的接口元素,但是由于Filter并非继承自Processor,因此不能将Filter用于Apply.process()方法上。这里主要是因为Apply.process()方法和Processor之间的耦合过紧,已经超出了所需要的程度,这就使得应该复用Apply.process()的代码时,复用却被禁止了。
如何解决这个问题呢,如果Processor是一个接口,那么这些限制就会变得松动,使得Filter用于Apply.process()方法上。下面是修改版本:
import java.util.Arrays; //接口 interface Processor{ public String name(); public Object process(Object input); } class Apply { //该方法作用于Processor类及其子类 public static void process(Processor p,Object s) { System.out.println("Using processor " + p.name()); System.out.println(p.process(s)); } } /****** 复用代码第一种方式是客户端程序员遵循该接口来编写它们自己的类 */ public abstract class StringProcessor implements Processor{ public String name() { return getClass().getSimpleName(); } //这句不能少 public abstract String process(Object input); public static String s = "Disagreement with beliefs is my definition incorrect"; public static void main(String[] args) { Apply.process(new Upcase(),s); Apply.process(new Downcase(),s); Apply.process(new Splitter(),s); } } //子类 Object转换为大写字符串 class Upcase extends StringProcessor{ public String process(Object input) { return ((String)input).toUpperCase(); } } //子类 Object转换为小写字符串 class Downcase extends StringProcessor{ public String process(Object input) { return ((String)input).toLowerCase(); } } //子类 Object转换为数组 class Splitter extends StringProcessor{ public String process(Object input) { return Arrays.toString(((String)input).split(" ")); } }
输出结果:
Using processor Upcase DISAGREEMENT WITH BELIEFS IS MY DEFINITION INCORRECT Using processor Downcase disagreement with beliefs is my definition incorrect Using processor Splitter [Disagreement, with, beliefs, is, my, definition, incorrect]
但是大多数情况下是你无法修改你想要使用的类。例如,在电子滤波器的例子中,假设该类库是别人写好的,你只能使用,无法修改Filter,但是Apply.process()需要的是Processor类型的参数。在这种情况下,可以使用适配器设计模式。适配器中的代码将接受你所拥有的接口(Filter),并产生你所需要的接口(Processor),就像下面:
//适配器设计模式 class FilterAdapter implements Processor{ Filter filter; public FilterAdapter(Filter filter) { this.filter = filter; } public String name() { return this.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); Apply.process(new FilterAdapter(new BandPass(1.0,2.0)), w); } }
输出如下:
Using processor LowPass Waveform 0 Using processor HighPass Waveform 0 Using processor BandPass Waveform 0
四 Java中的多重继承
在Java中如果想要从一个非接口的类继承,那么只能从一个非接口的类继承,但是可以同时继承多个接口。并且需要将所有的接口名都置于implements关键字之后,用逗号将他们一一隔开。
下面的例子展示一个具体类:继承自一个非接口类和几个接口:
interface CanFight{ void fight(); } interface CanSwim{ void swim(); } interface CanFly{ void fly(); } class ActionCharacter{ public void fight() {} } class Hero extends ActionCharacter implements CanFight,CanSwim,CanFly{ public void swim() {} public void fly() {} } public class Adventure { public static void t(CanFight x) {x.fight();} public static void u(CanSwim x) {x.swim();;} public static void v(CanFly x) {x.fly();;} public static void w(ActionCharacter x) {x.fight();} public static void main(String[] args) { Hero h = new Hero(); t(h); u(h); v(h); w(h); } }
可以看到Hero继承自具体的类ActionCharacter和接口CanFight、CanSwim、CanFly。当通过这种方式将一个具体类和多个接口组合到一起时,这个具体类必须放到前面,后面跟着的才是接口。
注意:CanFight接口与ActionCharacter类中的fight()方法的特征签名是一样的,而且Hero中并没有提供fight()的定义。当想要创建对象时,所有的定义首先必须都存在。即使Hero没有显式的提供fight()的定义,其定义也会因ActionCharacter而随之而来,这样就使得创建Hero对象成为了可能。
五 通过继承来扩展接口
通过继承,可以很容易的在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。
interface Monster{ void menace(); } interface DangerousMonster extends Monster{ void destory(); } interface Lethal{ void kill(); } class DragonZilla implements DangerousMonster{ public void menace() {} public void destory() {} } interface Vampire extends DangerousMonster,Lethal{ void drinkBlood(); } class VerBadVampire implements Vampire{ public void menace() {} public void destory() {} public void kill() {} public void drinkBlood() {}; } public class HorrorShow { static void u(Monster b) {b.menace();} static void v(DangerousMonster d) { d.menace(); d.destory(); } static void w(Lethal l) {l.kill();} public static void main(String[] args) { DangerousMonster barney = new DragonZilla(); u(barney); v(barney); Vampire vlad = new VerBadVampire(); u(vlad); v(vlad); w(vlad); } }
六 适配接口
接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单的情况中,它的体现形式通常是一个接受接口类型的方法,可以接收如下参数:该接口或该接口的具体实现。
因此,接口的一种常见用法就是前面介绍到的策略设计模式,假设我们编写了一个执行某些操作的方法,而该方法将接受一个接口。只要我们创建一个遵循该接口的对象,就可以使用该方法来调用。
例如:Java SE5的Scanner类的构造器接受的就是一个Readable接口,因此只要我们创建一个类使其遵循Readable接口,就可以使Scanner作用于该类。
import java.util.*; import java.nio.*; public class RandomWords implements Readable{ private static Random rand = new Random(47); private static final char[] captials = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); private static final char[] lowers = "abcdefghijklmnopqrstuvwxyz".toCharArray(); private static final char[] vowels = "aeiou".toCharArray(); private int count; public RandomWords(int count) {this.count = count;} public int read(CharBuffer cb) { if(count-- == 0) { return -1; } cb.append(this.captials[rand.nextInt(this.captials.length)]); for(int i=0;i<4;i++) { cb.append(this.vowels[rand.nextInt(this.vowels.length)]); cb.append(this.lowers[rand.nextInt(this.lowers.length)]); } cb.append(" "); return 10; } public static void main(String[] args) { Scanner s = new Scanner(new RandomWords(10)); while(s.hasNext()) { System.out.println(s.next()); } } }
输出结果:
Yazeruyac Fowenucor Goeazimom Raeuuacio Nuoadesiw Hageaikux Ruqicibui Numasetih Kuuuuozog Waqizeyoy
Readable接口只需要实现read()方法,在read()函数内部,将输入内容添加到CharBuffer参数中,或者在没有输入时返回-1。
假设有一个未实现Readable接口的类(RandomDoubles类),怎么才能让Scanner作用于它呢?下面演示一个使用适配接口的案例:
import java.util.*; public class RandomDoubles { private static Random rand = new Random(47); public double next() {return rand.nextDouble();} public static void main(String[] args) { RandomDoubles rd = new RandomDoubles(); for(int i=0; i <7 ;i++) { System.out.println(rd.next() + ""); } } }
输出如下:
0.7271157860730044 0.5309454508634242 0.16020656493302599 0.18847866977771732 0.5166020801268457 0.2678662084200585 0.2613610344283964
适配的类可以通过继承和实现Readable接口来实现,因此通过使用interface关键字提供的伪多重继承机制,可以生成既是RandomDoubles又是Readable的新类,下面我们将创建一个适配器AdaptedRandomDoubles类,该类继承于实现类RandomDoubles和接口Readable,从而使得Scanner可以作用于AdaptedRandomDoubles类,进而作用于RandomDoubles类。
import java.util.*; import java.nio.*; public class AdaptedRandomDoubles extends RandomDoubles implements Readable{ private int count; public AdaptedRandomDoubles(int count) {this.count = count;} public int read(CharBuffer cb) { if(count-- == 0) { return -1; } //产生一个double类型数 String result = Double.toString(this.next()) + " "; cb.append(result); return result.length(); } public static void main(String[] args) { Scanner s = new Scanner(new AdaptedRandomDoubles(7)); while(s.hasNextDouble()) { System.out.println(s.nextDouble() + " "); } } }
输出结果:
0.7271157860730044 0.5309454508634242 0.16020656493302599 0.18847866977771732 0.5166020801268457 0.2678662084200585 0.2613610344283964
七 接口中的字段
在接口中定义的字段隐式地是static和final,并且修饰符是public,在声明时必须初始化,可以使用非常量表达式初始化。接口中的字段必然是常量,只能读不能改,这样才能为实现接口的对象提供一个统一的属性。(通俗的讲,你认为是要变化的东西,就放在你自己的实现中,不能放在接口中去,接口只是对一类事物的属性和行为更高层次的抽象。对修改关闭,对扩展(不同的实现implements)开放,接口是对开闭原则的一种体现)
//接口中的字段 必须初始化 import java.util.*; interface RandomVals{ Random RAND = new Random(47); //常量 int RANDOM_INT = RAND.nextInt(10); long RANDOM_LONG = RAND.nextLong()*10; float RANDOM_FLOAT = RAND.nextLong()*10; double RANDOM_DOUBLE = RAND.nextDouble()*10; } public class TestRandVals implements RandomVals { public static void main(String[] args) { System.out.println(RANDOM_INT); System.out.println(RandomVals.RANDOM_INT); System.out.println(RANDOM_LONG); System.out.println(RandomVals.RANDOM_LONG); System.out.println(RANDOM_FLOAT); System.out.println(RandomVals.RANDOM_FLOAT); System.out.println(RANDOM_DOUBLE); System.out.println(RandomVals.RANDOM_DOUBLE); } }
输出结果如下:
8 8 -32032247016559954 -32032247016559954 -8.5939291E18 -8.5939291E18 5.779976127815049 5.779976127815049
由于这些字段都是static的,它们在类第一次被加载时初始化,这发生在任何字段首次被访问时。这些字段不是接口的一部分,它们的值被存储在接口的静态存储区域内。
八 嵌套接口
接口可以嵌套在类或其他接口中,下面演示一个案例:
//嵌套接口 class A{ interface B{ void f(); } public class BImp implements B{ public void f() { } } private class BImp2 implements B{ public void f() {} } interface C{ void f(); } public class CImp implements C{ public void f() {} } private class CImp2 implements C{ public void f() {} } private interface D{ void f(); } private class DImp implements D{ public void f() {} } public class DImp2 implements D{ public void f() {} } public D getD(){ System.out.println("getD()"); return new DImp2(); } private D dRef; public void receiveD(D d) { dRef = d; dRef.f(); } } interface E{ interface G{ void f(); } public interface H{ void f(); } void g(); } public class NestingInterfaces { public class BImp implements A.B{ public void f() {} } class CImp implements A.C{ public void f() {} } class EImp implements E{ public void g() {} } class EGImp implements E.G{ public void f() {} } class Eimp2 implements E{ public void g() {} class EG implements E.G{ public void f() {} } } public static void main(String[] args) { A a = new A(); //can't access A.d //A.D ad = a.getD(); //Doesn't return anything but A.D //A.DImp2 di2 = a.getD(); //Can't access a member of the interface //a.getD().f(); A a2 = new A(); a2.receiveD(a.getD()); } }
在类中嵌套接口的语法是相当显而易见的,就像非嵌套接口一样,可以拥有public和“包访问”两种可视性。
作为一种新添加的方式,接口也可以被实现为private,就像在A.D中所看到的。那么private的嵌套接口有什么好处么?
private表示它是类A中一个私有接口,DImp展示了接口A.D它被实现为private,但是.DImp2展示了它同样可以被实现为public。但是A.DImp2在向上转型为A.D时无法在类的外部使用。
getD()方法:它是一个返回对private接口的引用的public方法。在main()中可以看到数次尝试使用返回值的行为都失败了。
接口E说明接口彼此之间也可以嵌套,但是嵌套在一个接口中的接口默认是public的,而不能声明为private,因此private在类外部是无法被实现的。
需要注意的是:在实现一个接口时,并不需要实现嵌套在其内部的任何接口,并且private接口不能在定义它的类之外被实现。
九 接口和工厂
接口时实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。这与直接调用接口实现类的构造器不同,我们在工厂对象上调用的是new 接口实现类() 的方法,用来生成接口的某个实现的对象。
interface Service{ void method1(); void method2(); } interface ServiceFactory{ Service getService(); } class Implementations1 implements Service{ Implementations1(){} public void method1() { System.out.println("Implementation1 method1"); } public void method2() { System.out.println("Implementation1 method2"); } } class Implementation1Factory implements ServiceFactory{ public Service getService() { return new Implementations1(); } } class Implementations2 implements Service{ Implementations2(){} public void method1() { System.out.println("Implementation1 method1"); } public void method2() { System.out.println("Implementation1 method2"); } } class Implementation2Factory implements ServiceFactory{ public Service getService() { return new Implementations2(); } } 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()); } }
输出结果:
Implementation1 method1 Implementation1 method2 Implementation1 method1 Implementation1 method2
参考文献:
[1]Java编程思想
[2] Java中接口里定义的成员变量