精华内容
下载资源
问答
  • Effective Java 类和接口 第16条:复合优先于继承 继承(inheritance)是实现代码重用的有力手段,但它并非永远是完成这项任务的最佳工作。使用不当会导致软件变得很脆弱。在包的内部使用继承是非常安全的,在那里,...

    Effective Java 类和接口 第16条:复合优先于继承

    继承(inheritance)是实现代码重用的有力手段,但它并非永远是完成这项任务的最佳工作。使用不当会导致软件变得很脆弱。在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处于同一个程序员的控制下。对于专门为了继承而设计的并且具有很好的文档说明的类来说,使用继承也是非常安全的。然而,对于普通的具体类进行跨超包边界的继承则是非常危险的。

    与方法调用不同的是,继承打破了封装性。子类信赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了这种变化,子类有可能会被破坏,即使它的代码完全没有改变。

    
    // Broken - Inappropriate use of inheritance!
    public class InstrumentedHashSet<E> extends HashSet<E> {
        // The number of attempted element insertions
        private int addCount = 0;
    
        public InstrumentedHashSet() {
        }
    
        public InstrumentedHashSet(int initCap, float loadFactor) {
            super(initCap, loadFactor);
        }
    
        @Override
        public boolean add(E e) {
            addCount++;
            return super.add(e);
        }
        @Override
        public boolean addAll(Collections<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
        public int getAddCount() {
            return addCount;
        }
    

    这个类非常合理,但是他不能正常工作。

    例:

    InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
    s.addAll(Arrays.asList("a", "b", "c");
    

    我们期望getAddCount方法将会返回3,但结果却是6,原因是在HashSet内部,addAll方法是基于它的add方法实现的,InstrumentedHashSet的addAll方法首先给addCount增加3,然后利用super.addAll调用HashSet的addAll实现,然后有此次调用到被InstrumentedHashSet覆盖了的add方法,每个元素调用一次,每次调用增加1,所有总共增加6。

    只要去掉覆盖的addAll方法,就可以“修正”这个子类,使其正常工作,但是HashSet的addAll方法会随着版本更新而改变。
    因此InstrumentedHashSet类,是脆弱的。

    导致子类脆弱的一个相关的原因是:它们的超类在后续的发行版本中可以获得新的方法,假设一个程序的安全性信赖于这样的事实:所有被插入到某个集合的元素都满足某个先决条件。下面的做法就可以确保这一点:对集合进行子类化,并覆盖所有能够添加元素的方法以便确保在加入每个元素之前它是满足这个先决条件的。如果在后续的发行版本中,超类中没有增加能插入元素的新方法,那么这种方法可以正常工作。然而,一旦超类增加了这样的新方法,则很可能仅仅由于调用了这个未被子类覆盖的新方法,而将不合法的元素添加到子类的实例中。

    这两个问题的来源都是因为“覆盖”。如果在扩展一个类的时候仅仅是增加新的方法而不覆盖现有的方法,这也许看来相对安全一些,但是设想一下,如果超类在后续的发行版本中获得了一个新方法,并且和子类中的某一方法只是返回类型不同,那么这样的子类将针法通过编译。如果给子类提供的方法带有与新的超类方法完全相同的方法(签名和返回类型都相同),这又变成了子类覆盖超类的方法问题。此外,子类的方法是否则够遵守新的超类的方法的约定也是个值得怀疑的问题,因为当编写子类方法的时候,这个约定根本还没有面世。

    解决方法:
    使用“复合(composition)”,不用扩展现有的类,而是在新的类中增加一个私有域。

    // Wrapper class - uses composition in place of inheritance
    public class InstrumentedSet<E> extends ForwardingSet<E> {
        private int addCount = 0;
    
        public InstrumentedSet(Set<E> s) {
            super(s);
        }
    
        @Override
        public boolean add(E e) {
            addCount++;
            return super.add(e);
        }
    
        @Override
        public boolean addAll(Collection<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
    
        public int getAddCount() {
            return addCount;
        }
    }
    
    // Reusable forwarding class
    public class ForwardingSet<E> implements Set<E> {
        private final Set<E> s;
        public ForwardingSet(Set<E> s) { this.s = s; }
    
        public void clear() { s.clear(); }
        public boolean contains(Object o) { return s.contains(o); }
        public boolean isEmpty() { return s.isEmpty(); }
        public int size() { return s.size(); }
        public Iterator<E> iterator() { return s.iterator(); }
        public boolean add(E e) { return s.add(e); }
        public boolean remove(Object o) { return s.remove(o); }
        public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
        public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
        public boolean removeAll(Collection<?> c) { return s.removeAll(c); }
        public boolean retainAll(Collection<?> c) { return s.retainAll(c); }
        public Object[] toArray() { return s.toArray(); }
        public <T> T[] toArray(T[] a) { return s.toArray(a); }
        @Override
        public boolean equals(Object o) { return s.equals(o); }
        @Override
        public int hashCode() { return s.hashCode(); }
        @Override
        public String toString() { return s.toString(); }
    }

    Set接口的存在使得INstrumentedSet类的设计成为可能,因为Set接口保存了HashSet类的功能特性。除了获得健壮性之外,这种设计也带来了格外的灵活性。InstrumentedSet类实现了Set接口,并且拥有单个构造器,它的参数也是Set类型。从本质上讲,这个类把一个Set转变成了另一个Set,同时增加了计数的功能。前面提到的基于继承的方法只适用于单个具体的类,并且对于超类中所支持的每个构造器都要求有一个单独的构造器,与此不同的是,这里的包装类(wrapper class)可以被用来包装任何Set实现,并且可以结合任何先前存在的构造器一起工作。例如:

    Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));
    
    Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));
    

    InstrumentedSet甚至也可以用来临时替换一个原本没有计数特性的Set实例:

    static void walk(Set<Dog> dogs) {
    
    InstrumentedSet<Dog> iDogs = new InstrumentedSet<Dog>(dogs);
    
    ... // within this method use iDogs instead of dogs
    
    }

    因为每一个InstrumentedSet实例都把另一个Set实例包装起来了,所以InstrumentedSet类被称做包装类(wrapper class)。这也正是Decorator模式,因为InstrumentedSet类对一个集合进行了修饰,为他增加了计数特性。有时候,复合和转发的结合也被错误的称为“委托(delegation)”。从技术的角度而言,这不是委托,除非包装对象把自身传递给被包装的对象。

    包装类几乎没有什么缺点。需要注意的一点是,包装类不合适用在回调框架(callback framework)中;在回调框架中,对象把自身的引用传递给其他的对象,用于后续的调用(“回调”)。因为被包装起来的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时避开了外面的包装对象。这被称为SELF问题。有些人担心转发方法调用所带来的性能影响,或者包装对象导致的内存占用。在实践中,这两者都不会造成很大的影响。编写转发方法倒是有些琐碎,但是只需要给每个接口编写一次构造器,转发类则可以通过包含接口的包替你提供。

    只有当子类真正是超类的子类型(subtype)时,才适合继承。换句话说,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该扩展A。如果你打算让B扩展A,就应该问问自己:每个B确实也是A吗?如果你不能够确定这个问题的答案是肯定的,那么B就不应该扩展A。如果答案是否定的,通常情况下,B应该包含A的一个私有实例,并且暴露一个较小的、叫简单的API:A本质上不是B的一部分,只是他的实现细节而已。

    在Java平台类库中,有许多明显违反这条原则的地方。例如,栈(stack)并不是向量(vector),所以Stack不应该扩展Vector。同样的,属性列表也不是散列表,所以Properties不应该扩展Hashtable。在这两种情况下,复合模式才是恰当的。

    如果在适合于使用复合的地方使用了继承,则会不必要地暴露实现细节。这样得到的API会把你限制在原始的实现上,永远限定了类的性能。更为严重的说,由于暴露了内部的细节,客户端就有可能直接访问这些内部细节。这样至少会导致语义上的混淆。

    在决定使用继承而不是复合之前,还应该问自己最后一组问题。对于你正试图扩展的类,它的API中有没有缺陷呢?如果有,你是否愿意把那些缺陷传播到类的API中?继承机制会把超类API中的所有缺陷传递到子类中,而复合则允许设计新的API来隐藏这些缺陷。

    简而言之,继承的功能非常强大,但是也存在诸多问题。因为它违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性(fragility)。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当地接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。

    展开全文
  • java程序设计语言提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象。这两种机制之间最明显的区别在于,抽象允许包含某些方法的实现,但是接口不允许,一个更重要的区别在于,为了实现抽象定义的类型...

    java程序设计语言提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。这两种机制之间最明显的区别在于,抽象类允许包含某些方法的实现,但是接口不允许,一个更重要的区别在于,为了实现抽象定义的类型,类必须成为抽象类的子类,任何一个类,只要它定义了所有必要的方法,并且遵守通用约定,它就被允许实现一个接口,而不管这个类是处于类层次的那个位置。可以Java只允许单继承,所以抽象类作为类型定义受到了极大的限制。

    现有的类可以很容易被更新,以实现新的接口。
    如果这些方法尚不存在,你需要做的就只是增加必要的方法,然后在类的声明中增加一个implement子句。
    如果你希望实现两个类扩展同一个类的抽象类,就必须把抽象类放到类型层次的高处,以便这两个类的一个祖先成为它的子类。遗憾的是,这样做会间接的伤害到类层次,迫使这个公共祖先的所有的后代类都扩展了这个新的抽象类,无论它对于这些后代类是否合适。

    接口是定义mixin(混合类型)的理想选择。
    不严格的来讲,mixin是指这样的类型:类除了实现它的“基本类型”之外,还可以实现这个mixin类型,以表明它提供了某些可选择的行为。这样的接口之所以被称为mixin,是因为它允许任选的功能可被混合到类型的主要功能中。抽象类不能够被定义为mixin,同样也是因为它们不能被更新到现有的类:类不可能有一个以上的父类,类层次结构中也没有适当的地方来插入mixin。

    接口允许我们构造非层次结构的类型框架。
    类型层次对于组织某些事物是非常合适的。但是其他有些事物并不能被整齐地组织成一个严格的层次结构。

    例:假设我们有一个接口代表一个singer,另一个接口代表一个songweiter。

    public interface Singer{
     AudioClip sing(Song s);
    }
    public interface Songwriter{
     Song compose(boolean hit);
    }

    在现实生活中,有些歌唱家本身也是作曲家。因为我们使用了接口而不是抽象类来定义这些类型,所以对于单个类而言,它同时实现Singer和Songwriter是允许的,实际上我们可以定义第三个接口,他同时扩展了Singer和Songwriter,并添加了一些适合于这种组合的新方法:

    public interface SingerSongwriter extends Singer, Songwriter{
     AudioClip strum();
     void actSensitive();
    }
    

    你并不是总是需要这种灵活性,但是一旦你这样做了,接口可就成为了救世主,能帮助你解决大问题。另外一种做法是编写一个臃肿的类层次,对于每一种要被支持的属性组合,都包含一个单独的类。如果在整个类型系统中有n个属性,那么必须支持2^n种可能的组合。这种现象被称为“组合爆炸”。类层次的臃肿也导致类也臃肿,这些类也包含许多方法,并且这些方法只是在参数的类型上有所不同而已,因为类层次中没有任何类型体现公共的行为特征。

    通过第16条中介绍的包装类模式,接口是个安全的增加类的功能称为可能。如果使用抽象类来定义类型,那么程序员除了使用继承的手段来增加功能,没有其他的选择,这样得到的类与包装相比,功能更差,也更加脆弱。

    虽然接口不允许包含方法的实现,但是接口来定义类型并不妨碍你为程序员提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象的骨架实现类。把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。

    按照惯例,骨架实现被称为AbstractInterface,这里的Interface是指所是实现的接口的名字。例如Collections、Framework为每个重要的集合接口都提供了一个骨架实现,包括AbstractCollection、AbstractSet、AbstractList和AbstractMap。将他们称作SkeletalCollection、SkeletalSeet、SkeletalList和SkeletalMap也是有道理的,但是现在Abstract的用法已经根深蒂固。

    如果设计得当,骨架实现可以使程序员很容易提供他们的接口实现。

    例:一个静态工厂方法,它包括一个完整的,功能全面的List实现:

    //Concrete implementation built atop skeletak implementation
    static List<Integer> intArrayAsList(final int[] a){
        if(a == null )
            throw new NullPointerException();
        return new AbstractList<Integer>(){
            public Integer get(int i){
                return a[i];//Autoboxing (Item s)
            }
    
            @Override
            public Integer set(int i,Integer val){
                int oldVal=a[i];
                a[i]=val;//Anto-unboxing
                return oldVal;//Antoboxing
            }
            public int size(){
                return a.length;
            }
        };
    }

    当你考虑一个List实现应该为你完成那些工作的时候,可以看出,这个例子充分演示了骨架实现的强大功能。

    骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制。对于接口的大多数实现来讲,扩展骨架实现类是个很明显的选择,但不是必须的。如果预置的类无法扩展骨架实现类,这个类始终可以手工实现这个接口。此外,骨架实现类仍然能够有助于接口的实现。实现这个接口的类可以把对于接口方法的调用,转发到一个内部私有类的实例上,这个内部私有类扩展了骨架实现类。这种方法被称作模拟多态继承,它与第16条中讨论的包装类模式密切相关。这项技术具有多重继承的绝大多数有点,同时又避免了相应的缺陷。

    编写骨架实现类相对比较简单,只是有点单调乏味。首先,必须认真研究接口,并确定哪些方法是最为基本的,其他方法则可以根据它们来实现。这些基本方法将成为骨架实现类中的抽象方法。然后,必须为接口中的所有其他的方法提供具体实现。

    例:Map.Entry接口的骨架实现类

    public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
        //Primitive operations
        public abstract K getKey();
    
        public abstract V getValue();
    
        //Entries in modifiable maps must override this method
        public V setValue(V value) {
            throw new UnsupportedOperationException();
        }
    
        //Implements the general contract of Map.Entry.equals
        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }
            if (!(obj instanceof Map.Entry)) return false;
            Map.Entry<?, ?> arg = (Map.Entry<?, ?>) obj;
            return equals(getKey(), arg.getKey())
                    && equals(getValue(), arg.getValue());
        }
    
        private static boolean equals(Object o1, Object o2) {
            return o1 == null ? o2 == null : o1.equals(o2);
        }
    
        //Implements the general contract of Map.Entry.equals
        @Override
        public int hashCode() {
            return hashCode(getKey()) ^ hashCode(getValue());
        }
    
        private static int hashCode(Object obj) {
            return obj == null ? 0 : obj.hashCode();
        }
    }

    **因为骨架实现类是为了继承的目的而设计的,所以应该遵守第17条中介绍的所有关于设计和文档的指导原则。
    骨架实现有个小小不的不同,就是简单实现(simple implementation),**AbstractMap.SimpleEntry就是个例子。简单实现就像个骨架实现,这是因为它实现了接口,并且是为了继承而设计的,但是区别在于它不是抽象的:它是最简单的可能的有效实现。你可以原封不动的使用,也可以看情况将它子类化。

    抽象类相对于接口有一个优势:抽象类的演变比接口的演变要容易的多。如果在后续的发行版本中,你希望在抽象类中增加新的方法,始终可以增加具体方法,它包含合理的默认实现。然后,该抽象类的所有实现都将提供这个新的方法。对于接口,这样做是行不通的。因此设计共有接口要非常谨慎,接口一旦被公开发行,并且已被广泛实现,在想改变这个接口几乎是不可能的。

    展开全文
  • 类和接口Java程序设计语言的核心,也是java语言的基本抽象单位。java语言提供许多强大基本元素,供我们来设计接口。怎么才能设计出更加有用,健壮和灵活的类和接口? 首先区别设计良好的模块与设计不好的模块...
    类和接口是Java程序设计语言的核心,也是java语言的基本抽象单位。java语言提供许多强大基本元素,供我们来设计接口。怎么才能设计出更加有用,健壮和灵活的类和接口?


    首先区别设计良好的模块与设计不好的模块,区别的因素就是该模块对于外部的其他模块而言,是否隐藏其内部数据和其他实现细节。


    软件设计的基本原则: 封装/信息隐藏:设计良好的模块会隐藏所有的实现细节,把他的API与它的实现清晰地分隔,模块之间只能通过他们的API进行通信,不需要知道其他模块的内部工作情况。


    正确的使用修饰符对于实现信息隐藏是非常关键:
    private --该成员的顶层类内部才可以访问。

    package-private(缺省) --该成员包内部的任何类可以访问。

    protected --该成员的类与子类可以访问这个成员,并且该成员包内部的任何类也可以访问。

    public --任何地方度可以访问

    总而言之,我们应该始终尽可能的降低可访问性。
    展开全文
  • 实现接口时,接口就充当可以引用这个类型的实例的类型。因此,实现了接口,就表明客户端可以对这个的实例实施某些动作。为了任何其他目的而定义接口时不恰当的。 有一种叫做常量接口,他不满足上面的条件...

    当类实现接口时,接口就充当可以引用这个类型的实例的类型。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口时不恰当的。

    有一种类叫做常量接口,他不满足上面的条件。这种接口没有包含任何方法,他只包含静态的final域,每个域都导出一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名。
    下面是一个例子:

    //Constant interface antipattern - do not use!
    public interface PhysicalConstants{
    
        //阿伏伽德罗数
        static final double AVOGADROS_NUMBER = 6.02214199e23;
    
        //玻尔兹曼常数
        static final double BOLRZMANN_CONSTANT = 1.3806503E-23;
    
        //电子质量
        static final double ELECTRON_MASS = 9.10938188E-31;
    }

    常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄露到该类的导出API中。类实现常量接口,这对于用户来讲并没有什么价值。实际上,这样做反而会使他们更加糊涂。更糟糕的是,他代表了一种承诺:如果将来的发行版本中,这个类被修改了,他不再需要使用这些常量了,他依然必须实现这个接口,以确保二进制兼容性。如果非final类实现了常量接口,他的所有子类的命名空间也会被接口中的常量所“污染”。

    在java平台类库中有几个常量接口,例如java.io.ObjectStreamConstants。这些接口应该被认为是反面的典型,不值得被效仿。

    如果要导出常量,可以有几种合理的选择方案。如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中。例如,在java平台类库中所有的数值包装类,比如Integer和Double,都导出了MIN_VALUE和MAX_VALUE常量。如果这些常量最好被看做枚举类型的成员,就应该用枚举类型(enum type)来导出这些常量。否则,应该使用不可实例化的工具类(utility class)(见4条)来导出这些常量。

    例:

    public class PhysicalConstants {
    
        private PhysicalConstants(){}
    
        public static final double AVOGADROS_NUMBER = 6.02214199e23;   
        public static final double BOLRZMANN_CONSTANT = 1.3806503E-23;
        public static final double ELECTRON_MASS = 9.10938188E-31;
    }

    工具类通常要求客户端要用类名来修饰这些常量名,例如PhysicalConstants.AVOGADROS_NUMBER。如果大量利用工具类导出的常量,可以通过利用静态导入(static import)机制。避免用类名来修饰常量名,不过静态导入机制是在java发行版本1.5中才引入的:

    public class test {
        double atoms(double mols){
            return AVOGADROS_NUMBER * mols;
        }
    }

    简而言之,接口应该只被用来定义类型,他不应该被用来导出常量。

    展开全文
  • 有时候,可能会遇到带有两种甚至多种风格的实例的,并包含表示实例风格的标签(tag)域,例如,考虑下面这个,它能够表示圆形或者矩形: // Tagged class - vastly inferior to a class hierarchy! class ...
  • 常用的java类和接口1

    千次阅读 2012-04-19 21:19:05
    Java 应用程序编程接口  Java 应用程序编程接口(Application Programming Interface,API)是Sun公司开发... Java系统提供了大量的类和接口供程序开发人员使用,并且按照功能的不同,存放在不同的包中。这些包的集合
  • 嵌套(nested class)是指被定义在另一个的内部的。嵌套存在的目的应该只是为他的外围(enclosing class)提供服务。如果嵌套将来可能会用于其他的某个环境中,他就应该是顶层(top-level class)。...
  • 在书上看到,对于实现了Comparable接口,Arrays中的sort方法就可以对其进行排序, 那么,我们自己写的接口也能有这样的功能吗? 即,自己声明一个接口并定义一个方法,对于实现了该接口的其他,那么就可以...
  • Java平台类库中包含许多不可变的,其中就有String,基本类型的包装。BigIntegerBidDdcimal。存在不可变的有有许多理由:(相对于可变的) 1.容易设计,实现使用, 2.不容出错,且更加安全。实现不可变...
  • 为了在java中实现这种模式,要声明一个接口来表示该策略,并且为每一个具体策略声明一个实现了该接口。当一个具体策略只被使用一次时,通常使用匿名来声明实例化这个具体策略。当一个具体策略时设计用来...
  • 一位学习C语言(那种C语言不太清楚)的同事问我,为什么.属性可以解决的事情,为什么要get与set方法,当时我懵了。如下:class Point{ public double x; public double y; }调用:Point p=new Point(); p.x=10; ...
  • 一个可以实现多个接口: class D implements A,B,C{} 但是一个只能继承一个,不能继承多个 class B extends A{} 在继承的同时,也可以继承接口: class E extends D implements A,B,C{} 这也正是选择用接口...
  • 第16条提醒我们,对于不是为了继承而设计,并且没有文档说明的“外来”进行子类化是多么危险。...(java.util.AbstractCollection) /** * Removes a single instance of the specified el...
  • 本文结合《Effective Java》第四章《类和接口》和自己的理解及实践,讲解了设计Java类和接口的优秀指导原则,文章发布于专栏Effective Java,欢迎读者订阅。
  • java接口和类的区别Java 接口

    万次阅读 2016-11-07 20:47:55
    接口(interface)在java语言中就是一个抽象类型,但接口并不是一个抽象,是抽象方法的集合,接口通常以interface来声明。一个通过继承接口的方式,从而来继承接口的抽象方法。 接口并不是,编写接口的方式...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 92,485
精华内容 36,994
关键字:

java类和接口

java 订阅