接口

822 查看

接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法

抽象类和抽象方法

抽象类可以理解为是一种不够纯粹的接口,它是普通类与接口之间的一种中庸之道。

再论初始化

首先看看下面的实例代码:


class Glyph { void draw() { print("Glyph.draw()"); } Glyph() { print("Glyph() before draw()"); draw(); print("Glyph() after draw()"); } } class RoundGlyph extends Glyph { private int radius = 1; RoundGlyph(int r) { radius = r; print("RoundGlyph.RoundGlyph(), radius = " + radius); } void draw() { print("RoundGlyph.draw(), radius = " + radius); } } public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } /* Output: Glyph() before draw() RoundGlyph.draw(), radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph(), radius = 5 *///:~

根据Thinking in Java的描述:
1)在其它任何事物发生之前,将分配给对象的存储空间初始化为成二进制的零(基本类型按照其类型初始化,引用类型为null)
2)调用基类的构造器(如果基类中实例属性或者静态属性有初始化过程则先执行初始化过程),此时,调用被覆盖后的draw(),由于步骤1的缘故,
我们会发现radius的值为0.
3)按照声明的顺序调用成员的初始化方法
4)调用导出类的构造器主体
之所以出现上面的这种问题,是因为初始化的顺序,以及在构造器中调用了可以被子类重写的方法造成的。这种错误往往难以发现。如何避免这种
错误呢?在编写构造器时有一个重要的准则:“用尽可能简单的方法使对象进入正常状态:如果可以的话,避免调用其他方法”
在构造器中唯一能够安全调用的那些方法是基类中的final方法()(也适用于private方法,因为private方法自动属于final方法),

接口为Java提供了多重继承

使用接口的核心原因:
为了能够向上转型为多个基类型(以及由此带来的灵活性).看看下面的示例代码:


package c07.po; interface CanFight { void fight(); } interface CanClimb { void canClimb(); } interface CanSwim { void swim(); } interface CanFly { void fly(); } class ActionCharacter { public void fight() { } } class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly,CanClimb { @Override public void fly() { } @Override public void swim() { } @Override public void canClimb() { } } 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 c(CanClimb c) { c.canClimb(); } 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 c(h); // Treat it as a CanClimb } }

接口带给Java多重继承的特性,可以使一个对象C既可以被认作是A对象,也可以被认作是B对象,并且在被认作是A或者B对象的时候可以利用多态实
现不同对象的同一方法的不同的行为。这样就提高了设计的灵活性。并且在对象C作为方法参数时,最好使用它的基类型作为参数类型,这样对象c即
适用于以类型A作为参数类型的方法,也可以适用于类型B作为参数类型的方法,提高了灵活性。
可以这么说:接口是保证系统可扩展可插拔的基础(关键因素),一个设计良好的系统必须留有足够的接口。

接口是允许多继承的

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

package c07.po;

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);
    }
} // /:~


组合接口时的命名冲突

实现多重继承时,可能会遇到一个小麻烦。两个接口的包含方法签名和返回类型都完全一致的方法当然没有什么问题,但是下面的例子就有些麻烦了
先考虑一件事情:编译器是无法分别一个方法名和方法参数都相同仅有返回值不同的方法的。两个方法签名完全一致的方法会导致编译器报错。
关于方法签名可以这么简单的理解:方法的名字,方法的参数列表(包括参数类型和参数的顺序)组成方法的方法签名。
看看下面的示例代码:


public interface F1 { void f(); String g(); } public interface F2 { void f(int i ); int g(int g); } public interface F3 { int f(); } public class F1F2 implements F1, F2 { /** * 接口F1的方法void f() 与接口F2的方法void f(int i ) 名称相同但是参数列表不同,所以可以形成重载overload * 这是可行的,也就是不同接口两个方法同名但是参数列表不同 */ @Override public void f(int i) { } @Override public void f() { } /** * 接口F1的String g()和接口F2的int g(int g)只有方法名称相同,参数和返回值都不同,不形成重载 */ @Override public int g(int g) { return 0; } @Override public String g() { return null; } } public class F1F3 implements F1,F3 {//The type F1F3 must implement the inherited abstract method F3.f() /**ERROR * The return type is incompatible with F3.f() 接口F1的void f()方法与接口F3的方法int f()参数列表和名称完全相同,只有返回值不同,不能形成重载,但是也无法通过 编译,编译器无法分别两个同名同参的方法 所以类F1F3是无法同时兼容接口F1和F3的 当然这种蛋疼的事情还是很少遇见的。 @Override public void f() { // TODO Auto-generated method stub } */ @Override public String g() { return null; } }

方法签名

摘抄自一段提问
What is method signature in java?

The term "method signature" has an exact meaning in Java and is explicitly defined in the Java Language Specification.
The signature of a method consists of the method name and the parameter list (type and number). It DOES NOT include the return type or modifiers such as access modifiers (public, protected, , private), abstract, static, etc.

For example, you cannot have two methods with the same name and parameter list that differs only in that one is static and the other is not, or that differ in that one returns void and the other returns a non-void value.

The use of generics resulted in the addition of the term subsignature, which is documented in the latest Java Language Specification

原文链接:https://answers.yahoo.com/question/index?qid=20070306205943AAAsAnx

Java给出的官方文档的解释是这样的:

8.4.2. Method Signature

Two methods or constructors, M and N, have the same signature if they have the same name, the same type parameters (if any) (§8.4.4), and, after adapting the formal parameter types of N to the the type parameters of M, the same formal parameter types.

The signature of a method m1 is a subsignature of the signature of a method m2 if either:

m2 has the same signature as m1, or

the signature of m1 is the same as the erasure (§4.6) of the signature of m2.

Two method signatures m1 and m2 are override-equivalent iff either m1 is a subsignature of m2 or m2 is a subsignature of m1.

It is a compile-time error to declare two methods with override-equivalent signatures in a class.


class Point { int x, y; abstract void move(int dx, int dy); void move(int dx, int dy) { x += dx; y += dy; } } /** This program causes a compile-time error because it declares two move methods with the same (and hence, override-equivalent) signature. This is an error even though one of the declarations is abstract. **/

The notion of subsignature is designed to express a relationship between two methods whose signatures are not identical, but in which one may override the other. Specifically, it allows a method whose signature does not use generic types to override any generified version of that method. This is important so that library designers may freely generify methods independently of clients that define subclasses or subinterfaces of the library.

class CollectionConverter {
    List toList(Collection c) {...}
}
class Overrider extends CollectionConverter {
    List toList(Collection c) {...}
}

/**
Now, assume this code was written before the introduction of generics, and now the author of class CollectionConverter decides to generify the code, thus:
**/

class CollectionConverter {
    <T> List<T> toList(Collection<T> c) {...}
}

/**
Without special dispensation, Overrider.toList would no longer override CollectionConverter.toList. Instead, the code would be illegal. This would significantly inhibit the use of generics, since library writers would hesitate to migrate existing code.
**/

接口中的域

放入接口中的任何域都都自动是static 和final的,所以接口是一种便捷的用来创建常量的工具。在JavaSE5之前,这是产生enum类型的唯一途径
接口中的域不可以是“空final的”,但是可以被非常量表达式初始化。
接口的静态常量的初始化只有在接口加载的时候才会进行初始化,比如直接调用接口常量可以引起接口常量的初始化,再比如子类调用实现接口的方法
会引起接口常量的初始化.实例代码如下:


public class Value { private String str; public Value(String str){ this.str = str; System.out.println(this.str); } } public interface InterFaceA { public Value v1 = new Value("v1"); Value v2 = new Value("v2"); void printValue(); } public class TC { public static Value v1TC = new Value("v1TC"); public static Value v2TC= new Value("v2TC"); } public class ImplA extends TC implements InterFaceA{ public ImplA(){ System.out.println("ImplA构造函数来啦"); } @Override public void printValue() { System.out.println(v1); System.out.println(v2); } } package c07.ta; import static org.junit.Assert.*; import org.junit.Test; public class TestClient { /** * 直接访问接口常量,会引起接口常量的初始化(全部常量) */ @Test public void test1() throws Exception { Value v1 = ImplA.v1; //输出 : v1 v2 } /** * 只初始化接口子类,只会先初始化父类的静态常量,再初始化子类 * 并不会进行接口的初始化,也不会有接口静态常量的初始化 */ @Test public void test2() throws Exception { ImplA implA = new ImplA(); /*输出: * v1TC v2TC ImplA构造函数来啦 */ } /** *初始化接口子类,并且调用子类实现接口的方法,会引起接口的初始化,那么接口的静态常量也就初始化了 */ @Test public void test3() throws Exception { ImplA implA = new ImplA(); implA.printValue(); /*输出: v1TC v2TC ImplA构造函数来啦 v1 v2 c07.ta.Value@18fe7c3 c07.ta.Value@b8df17 */ } }

不要总是使用接口进行设计

“确定接口是理想的选择,因而应该总是选择接口而不是具体的类”这其实是一种诱惑。这种逻辑看起来是这样的:因为需要使用不同的具体实现,因此总应该添加这种抽象性
。这句话本身并没有错,但是却成为草率设计滥用接口的指导。
首先需要明确的一点是:任何抽象性都应该是由真正的需求而产生的。当必须时,你应该重构接口而不是到处添加额外级别的间接性,并由此带来额外的复杂性。
如果过度的对接口抽象,那么带来的后果则是类膨胀和复杂性的增加,因此既要保证系统的灵活性和可扩展性(利用多态特性,实现接口进行抽象),也要保证系统的复杂性不过分提高(慎用接口)。
恰当的原则是优先选择类而不是接口。从类开始,如果接口的必须性变得非常明确,那么就进行重构。接口是一种重要的工具,但是它们容易被滥用。
所以我认为合适的设计过程应该是先从划分类开始,先划分类的功能,再对类进行重新审视和设计,对类进行重构并且提炼出抽象类,在这基础上再进行接口的抽象,接口抽象后再进行审视,对接口进行重构,之后再审视接口与类,这样完成从底到上,在从顶到下的设计一个系统(子系统,模块,三五个类组成的组件)。

author zhaob
time : 2014-09-14 16:57