您的位置:首页 > 编程语言 > Java开发

读thinking in java笔记(八):接口

2016-09-13 19:56 429 查看
接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。

1. 抽象类和抽象方法

所谓抽象方法是指:仅有声明而没有方法体的方法,下面是抽象方法声明所采用的语法:

abstract void f();


包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。(否则编译器就会报错。)

如果从一个抽象类继承,并想创建该新类的对象,那么就必须为基类中的所有抽象方法提供实现。如果不这么做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类。

我们也可能会创建一个没有任何抽象方法的抽象类。考虑这种情况:如果有一个类,让其包含任何abstract方法都显得没有实际意义,而且我们也想要阻止产生这个类的任何对象,那么这时这样做就很有用了。

经过修改的“管弦乐器”的例子,其中采用了抽象类和抽象方法:
abstract class Instrument {
private int i; // Storage allocated for each
public abstract void play(Note n);
public String what() { return "Instrument"; }
public abstract void adjust();
}

class Wind extends Instrument {
public void play(Note n) {
print("Wind.play() " + n);
}
public String what() { return "Wind"; }
public void adjust() {}
}

class Percussion extends Instrument {
public void play(Note n) {
print("Percussion.play() " + n);
}
public String what() { return "Percussion"; }
public void adjust() {}
}

class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
public String what() { return "Stringed"; }
public void adjust() {}
}

class Brass extends Wind {
public void play(Note n) {
print("Brass.play() " + n);
}
public void adjust() { print("Brass.adjust()"); }
}

class Woodwind extends Wind {
public void play(Note n) {
print("Woodwind.play() " + n);
}
public String what() { return "Woodwind"; }
}

public class Music4 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
}


2. 接口

abstract关键字允许人们在类中创建一个或多个没有任何方法定义的方法,提供了接口部分,但是没有提供任何相应的具体实现,这些实现是由此类的继承者来做的。

interface这个关键字产生一个完全抽象的类,什么是完全抽象的类?即:接口中所有的方法都为抽象方法。interface不仅仅是一个极度抽象的类,因为它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继变种的特性。大白话说就是:你创建一个类,可以实现多个接口。(另外记住,你创建一个类,只能继承一个父类)

要想创建一个接口,需要用interface关键字来替代class关键字。就像类一样,可以在interface前面添加public关键字(但仅限于该接口在与其同名的文件中被定义)。如果不添加public关键字,则它只具有包访问权限,这样它就只能在同一个包内可用。接口也可以包含域,但是这些域隐式的是static和final的。

要让一个类遵循某个特定接口(或者是一组接口),需要使用implements关键字,它表示:interface只是它的外貌,但是现在我要声明它是如何工作的。除此之外,它看起来还很像继承。

可以选择在接口中显式地将方法声明为public的,但即使你不这么做,它们也是public的。因此,当要实现一个接口时,在接口中被定义的方法必须被定义为public的;

要注意的是,在接口中的每一个方法确实都只是一个声明,这是编译器所允许的在接口中唯一能够存在的事物。

3. 完全解耦

只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。你无法将这个方法应用于不在此继承结构中的某个类。接口可以在很大程度上放宽这种限制,因此,它使得我们可以编写可复用性更好的代码。

例如:假设有一个Processor类,它有一个name()方法;另外还有一个process方法,该方法接受输入参数,修改它的值,然后产生输出。这个类作为基类而被扩展,用来创建各种不同类型的Processor。在本例中,Processor子类将修改String对象(注意,返回类型可以是协变类型,而非参数类型):

class Processor {
public String name() {
return getClass().getSimpleName();
}
Object process(Object input) { return input; }
}

class Upcase extends Processor {
String process(Object input) { // Covariant return
return ((String)input).toUpperCase();
}
}

class Downcase extends Processor {
String process(Object input) {
return ((String)input).toLowerCase();
}
}

class Splitter extends Processor {
String process(Object input) {
// The split() argument divides a String into pieces:
return Arrays.toString(((String)input).split(" "));
}
}

public class Apply {
public static void process(Processor p, Object s) {
print("Using Processor " + p.name());
print(p.process(s));
}
public static String s =
"Disagreement with beliefs is by definition incorrect";
public static void main(String[] args) {
process(new Upcase(), s);
process(new Downcase(), s);
process(new Splitter(), s);
}
} /* Output:
Using Processor Upcase
DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT
Using Processor Downcase
disagreement with beliefs is by definition incorrect
Using Processor Splitter
[Disagreement, with, beliefs, is, by, definition, incorrect]
*/


Apply.process()方法可以接受任何类型的Processor,并将其应用到一个Object对象上,然后打印结果。像本例这样,创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。这类方法包含所要执行的算法中固定不变的部分,而策略包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。这里,Processor对象就是一个策略,在main中可以看到有三种不同类型的策略应用到了String类型的s对象上。

4. Java中的多重继承

接口不仅仅只是一种更纯粹的抽象类,它的目标比这更高。因为接口时根本没有任何具体实现的,也就是说,没有任何与接口相关的存储。因此,也就无法阻止多个接口的组合。这一点是很有价值的,因为你有时需要去表示“一个x是一个a和一个b以及一个c”。在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); // Treat it as a CanFight
u(h); // Treat it as a CanSwim
v(h); // Treat it as a CanFly
w(h); // Treat it as an ActionCharacter
}
}


可以看到,Hero组合了具体类ActionCharacter和接口CanFight、CanSwim和CanFly。当通过这种方式将一个具体类和多个接口组合到一起时,这个具体类必须放在前面,后面跟着的才是接口(否则编译器会报错)。

注意,CanFight接口与ActionCharacter类中的方法的特征签名是一样的,而且,在hero中并没有提供fight()的定义。可以扩展接口,但是得到的只是另一个接口。当想要创建对象时,所有的定义首先必须都存在。即使Hero没有显式地提供fight()的定义,其定义也因ActionCharacter而随之而来,这样就使得创建Hero对象成为可能。

在Adventure类中,可以看到有四个方法把上述各种接口和具体类作为参数。当Hero对象被创建时,它可以被传递给这些方法中的任何一个,这意味着它依次被向上转型为每一个接口。

一定要记住,前面的例子所展示的就是使用接口的核心原因:为了能够向上转型为多个基类型。然而,使用接口的第二个原因却是与使用抽象基类相同:防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口。这就带来一个问题:我们应该使用接口还是抽象类?如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。事实上,如果知道某事物应该成为一个基类,那么第一选择应该是使它成为一个接口。

5. 通过继承来扩展接口

通过继承,可以很容易的在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。这两种情况都可以获得新的接口,就像下面的例子中看到的:

interface Monster {
void menace();
}

interface DangerousMonster extends Monster {
void destroy();
}

interface Lethal {
void kill();
}

class DragonZilla implements DangerousMonster {
public void menace() {}
public void destroy() {}
}

interface Vampire extends DangerousMonster, Lethal {
void drinkBlood();
}

class VeryBadVampire implements Vampire {
public void menace() {}
public void destroy() {}
public void kill() {}
public void drinkBlood() {}
}

public class HorrorShow {
static void u(Monster b) { b.menace(); }
static void v(DangerousMonster d) {
d.menace();
d.destroy();
}
static void w(Lethal l) { l.kill(); }
public static void main(String[] args) {
DangerousMonster barney = new DragonZilla();
u(barney);
v(barney);
Vampire vlad = new VeryBadVampire();
u(vlad);
v(vlad);
w(vlad);
}
}


DangerousMonster是Monster的直接扩展,它产生了一个新接口。DragonZilla 中实现了这个接口。在Vampire 中使用的语法仅适用于接口继承。一般情况下,只可以将extends用于单一类,但是可以引用多个基类接口。就像所看到的,只需要用逗号将接口名分别隔开即可。

5.1 组合接口时的名字冲突

在实现多重继承时,可能会碰到一个小陷阱。在前面的例子中,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 1; } // overloaded
}

class C3 extends C implements I2 {
public int f(int i) { return 1; } // overloaded
}

class C4 extends C implements I3 {
// Identical, no problem:
public int f() { return 1; }
}


此时困难来了,因为覆盖、实现和重载令人不快的搅在一起,而且重载方法仅仅通过返回类型是区分不开的。在打算组合的接口中使用相同的方法名通常造成代码可读性的混乱,请尽量避免这种情况。

6. 适配接口

接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单的情况中,它的体现形式通常是一个接受接口类型的方法,而该接口的实现和向该方法传递的对象则取决于方法的使用者。

因此,接口的一种常见用法就是前面提到的策略设计模式,此时你编写一个执行某些操作的方法,而该方法将接受一个同样是你指定的接口。你主要就是要声明:“你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口”。这使得你的方法更加灵活、通用。

例如:JavaSE5的Scanner类的构造器接受的就是一个Readable接口。你会发现Readable没有用作Java标准类库中其他任何方法的参数,它是单独为Scanner创建的,以使得Scanner不必将其参数限制为某个特定类。通过这种方式,Scanner可以作用于更多的类型。如果你创建了一个新的类,并且想让Scanner作用于它,那么你就应该让它成为Readable,像下面这样:

public class RandomWords implements Readable {
private static Random rand = new Random(47);
private static final char[] capitals =
"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; // Indicates end of input
cb.append(capitals[rand.nextInt(capitals.length)]);
for(int i = 0; i < 4; i++) {
cb.append(vowels[rand.nextInt(vowels.length)]);
cb.append(lowers[rand.nextInt(lowers.length)]);
}
cb.append(" ");
return 10; // Number of characters appended
}
public static void main(String[] args) {
Scanner s = new Scanner(new RandomWords(10));
while(s.hasNext())
System.out.println(s.next());
}
} /* Output:
Yazeruyac
Fowenucor
Goeazimom
Raeuuacio
Nuoadesiw
Hageaikux
Ruqicibui
Numasetih
Kuuuuozog
Waqizeyoy
*/


Readable接口只要求实现read()方法,在read内部,将输入内容添加到CharBuffer参数中,或者再没有任何输入时返回-1。假设你有一个还未实现Readable的类,怎样才能让Scanner作用于它呢?下面这个类就是一个例子,它可以产生随机浮点数:

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.print(rd.next() + " ");
}
} /* Output:
0.7271157860730044  0.5309454508634242 0.16020656493302599 0.18847866977771732 0.5166020801268457  0.2678662084200585 0.2613610344283964
*/


我们再次使用了适配器模式,但是在本例中,被适配的类可以通过继承和实现Readable接口来创建。因此,通过使用interface关键字提供的伪多重继承机制,我们可以生成既是RandomDoubles又是Readble的新类:

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;
String result = Double.toString(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.print(s.nextDouble() + " ");
}
} /* Output:
0.7271157860730044 0.5309454508634242 0.16020656493302599 0.18847866977771732 0.5166020801268457 0.2678662084200585 0.2613610344283964
*/


因为在这种方式中,我们可以在任何现有类之上添加新的接口,所以,这意味着让方法接受接口类型,是一种让任何类都可以对该方法进行适配的方式。

7. 接口中的域

因为你放入接口中的任何域都自动是static和final的,所以接口就成为了一种很便捷的用来创建常量组的工具。在JavaSE5之前,这是产生与C或C++中的enum(枚举类型)具有相同效果的类型的唯一途径。因此在JavaSE5之前的代码中你会看到下面这样的代码:

public interface Months {
int
JANUARY = 1, FEBRUARY = 2, MARCH = 3,
APRIL = 4, MAY = 5, JUNE = 6, JULY = 7,
AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
NOVEMBER = 11, DECEMBER = 12;
}


请注意,Java中标识具有常量初始化值的static final时,会使用大写字母的风格(在一个标识符中用下划线来分隔多个单词)。接口中的域自动是public和final的,所以没有显式的地指明这一点。

有了JavaSE5,你就可以使用更加强大而灵活的enum关键字,因此,使用接口来群组常量已经显得没什么意义了。

7.1 初始化接口中的域

在接口中定义的域不能是“空final”,但是可以被非常量表达式初始化。例如:

public interface RandVals {
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 {
public static void main(String[] args) {
print(RandVals.RANDOM_INT);
print(RandVals.RANDOM_LONG);
print(RandVals.RANDOM_FLOAT);
print(RandVals.RANDOM_DOUBLE);
}
} /* Output:
8
-32032247016559954
-8.5939291E18
5.779976127815049
*/


既然域是static的,它们就可以在类第一次被加载时初始化,这发生在任何域首次被访问时。当然,这些域不是接口的一部分,它们的值被存储在该接口的静态存储区域内。

9.8 接口与工厂

接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。这与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。理论上,通过这种方式,我们的代码将完全与接口的实现分离,这就使得我们可以透明地将某个实现替换为另一个实现。

interface Service {
void method1();
void method2();
}

interface ServiceFactory {
Service getService();
}

class Implementation1 implements Service {
Implementation1() {} // Package access
public void method1() {print("Implementation1 method1");}
public void method2() {print("Implementation1 method2");}
}

class Implementation1Factory implements ServiceFactory {
public Service getService() {
return new Implementation1();
}
}

class Implementation2 implements Service {
Implementation2() {} // Package access
public void method1() {print("Implementation2 method1");}
public void method2() {print("Implementation2 method2");}
}

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());
// Implementations are completely interchangeable:
serviceConsumer(new Implementation2Factory());
}
} /* Output:
Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2
*/


如果不是工厂方法,你的代码就必须在某处指定将要创建的Service的确切类型,以便调用合适的构造器。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: