Effective Java读书笔记——第四章 类和接口

来源:互联网 发布:云计算工程师就业 编辑:程序博客网 时间:2024/05/21 21:33

第13条:使类和成员的可访问性最小化

略。。。

第14条:在共有类中使用访问方法而非公有域

class Point {     public double x;    public double y;}

上面的类没有对数据域进行封装,导致这些数据是可以被直接访问的,也就无法进行任何的约束条件。反正,应该对其进行封装:

class Point {    private double x;    private double y;    public Point(double x,double y) {        this.x = x;        this.y = y;    }    public double getX(){return x;}    public double getY(){return y;}    public void setX(double x){this.x = x;}    public void setY(double y){this.y = y;}}

注意:1、如果类是包级私有的,或者是私有的嵌套类,直接暴露数据域并没有什么不好。因为只有其外部类可以访问到,这对于调用者来说并没有影响。 2、如果类中的域是不可变的,难么影响会小一些,但也需要在赋值是适时地为它们做些限制:

public final class Time {    private static final int HOUR_PER_DAY = 24;    private static final int MINUTES_PER_HOUR = 60;    public final int hour;    public final int minute;    public Time(int hour,int minute) {        if(hour < 0 || hour >= HOUR_PER_DAY) {            throw new IllegalArgumentException("Hour: " + hour);        if(minute < 0 || hour >= MINUTES_PER_HOUR) {            throw new IllegalArgumentException("Minute: " + minute);        }        this.hour = hour;        this.minute = minute;    }}

第15条:使可变性最小化

不可变类是实例不可修改的类,一旦被创建、初始化,就不能在修改,如String类,包装器类、BigInteger、BigDecimal类等。

如自己设计不可变类,需满足如下规则:

1、不要提供任何可以修改对象状态的方法。

2、将类声明为final的。

3、使所有域都声明为final的。

4、所有的域都声明为私有的。

5、确保任何组件的互斥访问。(若类中有域是可变对象的引用,要确保客户端无法获取这样的引用。也不要使用客户端提供的对象引用来初始化这样的域,也不要哦从任何方法汇总返回该对象的引用)

public final class Complex {    private final double re;    private final double im;    public Complex(double re,double im) {        this.re = re;        this.im = im;    }    public double realPart() {        return re;    }    public double imaginaryPart() {        return im;    }    public Complex add(Complex c) {        return new Complex(re + c.realPart(),im + c.imaginaryPart());    }    public Complex subtract(Complex c) {        return new Complex(re - c.realPart(),im - c.imaginaryPart());    }    public Complex multiply(Complex c) {        return new Complex()    }       public Complex divide(Compare c) {        double tmp = c.realPart() * c.realPart() + c.imaginPart() * c.imaginPart();        return new Complex((re * c.realPart() + im * c.imaginPart())/tmp, (im * c.realPart() - re * c.imaginPart())/tmp);    }    @Override    public boolean equals(Object o) {        if(o == this) {            return true;        }        if(!(o instanceof Complex)) {            return false;        }        Complex c = (Complex)o;        return Double.compare(re ,c.realPart()) == 0 && Double.compare(im, c.imaginPart()) == 0;    }    @Override    public int hashCode() {        int result = 17 + hashDouble(re);        result = 31 * result + hashDouble(im);        return result;    }    private int hashDouble(double val) {        long longBits = Double.doubleToLongBits(re);        return (int)(longBits ^ (longBits) >>> 32));    }    @Override     public String toString() {        return "(" + re + " + " + im + "i)";    }}

这里的Complex类就是一个不可变类。提供的加减乘除的方法都是返回了新的Complex对象,而并不是修改这个实例。

不可变对象的好处之一是线程安全的,它们不要求同步。当多个线程并发访问这样的对象时,它们不会遭到破坏。即不可变对象可以自由地被共享。

所以不可变类应尽量多被重用,方法是为频繁用到的域提供公有的静态final常量,如可以为Complex提供:

public static final Complex ZERO = new Complex(0,0);public static final Complex ONE = new Complex(1,0);public static final Complex I = new Complex(0,1);

也可以把这些静态常量使用静态工厂的方式缓存起来,比如基本类型的包装类,或是BigInteger,都有这样的静态工厂。


为了确保不可变类的不可变性,有几种方式:一种是,将class声明成final的;还有一种,即让类的所有构造器都变成私有的或包级私有的,并添加静态工厂方法来代替公有构造器:

public class Complex {    private final double re;    private final double im;    private Complex(double re,double im) {        this.re = re;        this.im = im;    }    public static Complex valueOf(double re,double im) {        return new Complex(re,im);    }    ... ...}

静态工厂相比于构造器有很多优势,若希望提供一种基于极坐标创建复数的方式,使用构造器虽说可以,但与已有的构造器签名相同,造成了冲突(Complex(double , double))。而使用静态工厂方法就很容易做到:

public static Complex valueOfPolar(double r,double theta) {    return new Complex(r * Math.cos(theta), r * Math.sin(theta));}

对于有些类而言只能设计成可变的,对于这种类,仍然应该尽可能限制它的可变性。降低对象可以存在的状态数,降低出错可能性,除非有令人信服的理由要使域变成非final的,否则要使每个域都是final的。


第16条:复合优先于继承

与方法调用不同,继承打破了封装性(本条所说的继承不包括接口继承或是接口扩展另一个接口的情况),因为子类依赖了超类的特定实现。如果随着版本的迭代,超类发生了变化,子类也会跟着变化。举个栗子:

public class InstrumentedHashSet<E> extends HashSet<E> {    private int addCount = 0;    public InstrumentHashSet() {    }    public InstrumentHashSet(int initCap, float loadFactor) {    super(initCap , loadFactor);    }    @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;    }}

在客户端调用:

InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();s.addAll(Arrays.asList("snap","Crackle","Pop"));

这里,我们期望getAddCount方法会返回3,但是实际返回了6。因为addAll方法会首先将count加3,接着调用了父类(HashSet)的addAll方法,而这个方法又会调用被覆盖的add方法,该方法会在每次读到一个元素时加1,共被调用了3次,所以,返回了6。

解决方法是去掉被覆盖的addAll方法。这其实就是继承带来的与父类耦合性过高带来的问题。

使用组合可以避免覆盖带来的问题,即在类中增加一个私有域,引用现有类,而不是覆盖现有类。新类中每个方法都可以调用这个引用所指向对象中的方法,并返回结果。这样的结构非常稳固,它不依赖于现有类的实现细节,即便现有的类添加了新的方法,也不会影响到新的类。看个栗子:

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;    }}

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> interator() {        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.containAll(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();    }}

InstrumentedSet类组合了Set接口,当然也实现了Set接口,InstrumentedSet类的构造方法只传入Set接口作为参数,这增加了灵活性,您可以在初始化InstrumentedSet的时候传入任何Set的实现:

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

那么啥时候该用继承?

对于两个类A和B,只有当两者确实存在“is-a”关系的时候,类B才应该继承A。所以当每个B也是A的时候,才应该使用继承。否则的话,应该在B中包含一个A的私有实例。


第17条:要么为继承而设计,并提供文档说明,要么就禁止继承

如果需要继承某个类,就需要在文档中为重写的方法提供精确地描述,(即为每个覆写的方法所带来的影响做精确描述),比如在java.util.AbstractCollection类中的remove方法规范文档为:

//如果这个集合中存在指定的元素,就从中删除该指定元素中的单个实例(这是项可选操作)。更一般地,如果集合中包含一个或者多个这样的元素e,就从中删除这种元素,以便(o == null ? e == null : o.equals(e))。如果集合中包含指定的元素,就返回true(如果调用最终改变了集合,也一样)。该实现遍历整个集合来查询指定的元素。如果它找到该元素,将会利用迭代器的remove方法将之从集合中删除。注意,如果由该集合的iterator方法返回的迭代器没哟实现remove方法,该实现就会抛出UnsupportedOperationException。public boolean remove(Object o)

再看看java.util.AbstractList中的removeRange方法:

//从列表中删除所有索引处于fromIndex(含)和toIndex(不含)之间的元素。将所有符合条件的元素移到左边(减小索引)。这一调用将从ArrayList中删除fromIndex到toIndex之间的元素(若fromIndex==fromIndex,那么这项操作无效)这个方法是通过clear操作在这个列表及其子列表中调用的。覆盖这个方法来利用列表实现内部消息。可以充分改善这个列表中clear操作的性能。//参数://fromIndex 要移除的第一个元素的索引//toIndex 要溢出的最后一个元素的索引protected void removeRange(int fromIndex,int toIndex) 

继承类还需遵守一些约束:构造器不能调用可被覆盖的方法

public class Super {    public Super() {        overrideMe();    }    public void overrideMe() {        ...    }}
public final class Sub extends Super {    private final Date date;    Sub() {        date = new Date;    }    @Override    public void overrideMe() {        System.out.println(date);    }    public static void main(String[] args) {        Sub sub = new Sub();        sub.overrideMe();    }}

当执行时,本来期待这个程序回打印日期两次,但第一次打印出的是null,因为在Super的构造器中执行overrideMe()的时候,Sub类中的域date还没有被初始化。如果此时overrideMe还调用了date中的任何方法,当Super构造器调用overrideMe的时候,还会抛出NullPointerException异常。


如果某个类为继承而设计(该类作为父类),而这个还实现了Cloneable或是Serializable接口,那么不要在clone或是readObject方法中调用要被覆盖的方法,因为这两个方法类似于构造器。

如果某个类实现了Serializable接口,就必须把readResolve或writeReplace方法声明为受保护的方法。


第18条:接口由于抽象类

接口抽象类的区别是后者允许包含有具体实现的方法,而前者不行。最主要的区别是由于Java只允许单继承,但是接口可以实现多实现,即某个类可以实现多个接口,但是只能继承一个抽象类。

接口的优势:

  • 现有的类可以很容易被更新,以实现新的接口。:举个栗子,当Comparable接口被引入到Java平台时,会更新现有的类,只需要implements Comparable ,然后重写它的compareTo方法就行了,但是假设Comparable 是个抽象类,若干个需要继承Comparable 的类就必须让Comparable 抽象类成为它们共同的祖先,这样的话,就会破坏累的层次结构甚至架构,这样做的代价较大。

  • 接口是定义mixin(混合类型)的理想选择。:混合类型指的是,类实现了混了类型,以表明它提供了某个可供选择的行为。举个栗子,Comparable就是个混合接口,因为实现了Comparable接口的类就相当于跟外界表明“我是一个可以同类型比较大小的对象”,当然它还可以实现其他接口以表明其他身份,而抽象类不能被定义为混合类型,因为Java是单继承的。

  • 接口允许我们构造非层次结构的类型框架。:说白了,接口更加贴近现实生活,举个栗子,假设一个接口代表singer歌唱家,一个接口代表songwriter作曲家:

//歌唱家public interface Singer{    AudioClip sing(song s);}
//作曲家public interface SongWriter {    Song compose(boolean hit);}

在现实生活中,一个人可能既是歌手,也是作曲家,而且这个人其他独特的能力,那么接口的灵活性就能体现出来:

public interface SingerSongWriter extends Singer, SongWriter {    AudioClip strum();    void actSensitive();}

下面的静态工厂方法提供了List的完整功能实现:

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];        }        @Override         public Integer set(int i,Integer val) {            int oldVal = a[i];            a[i] = val;            return oldVal;        }        public int size() {            return a.length;        }    }}

上面的List实现成为一个骨架实现,优点是为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所持有的的严格限制。

下面看看Map.Entry接口的骨架实现:

public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {    public abstract K getKey();    public abstracr V getValue();    public V setValue(V value){        throw new UnsupportedOperationException();     }    @Override    public boolean equals(Object o) {        if(o == this) {            return true;        }        if(!(o instanceof Map.Entry))            return false;        Map.Entry<?,?> arg = (Map.Entry)o;        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);    }    @Override public int hashCode() {        return hashCode(getKey()) ^ hashCode(getValue());    }    private static int hashCode(Object obj) {        return obj == null ? 0 : obj.hashCode();    }}

第19条:接口只用于定义类型

一句话总结:不要在接口中定义常量。

第20条:类层次由于标签类

考虑下面这个Figure类,可以表示矩形或圆形:

class Figure {    enum Shape {        RECTANGLE, CIRCLE    };    final Shape shape;    double length;    double width;    double radius;    Figure(double radius) {        shape = Shape.CIRCLE;        this.radius = radius;    }    Figure(double length,double width) {        shape = Shape.RECTANGLE;        this.length = length;        this.width = width;    }    double area() {        switch(shape) {            case RECTANGLE:                return length * width;            case CIRCLE:                return Math.PI * (radius * radius);            default:                throw new AssertionError();         }    }}

Figure类是标签类,这种类有许多缺点,一句话总结:标签类过于冗长,容易出错,并且效率低下。


标签类带来问题的解决方式是:子类型化。

首先为标签类中的每个方法都定义一个包含该方法的抽象类,在Figure中只有area一个方法。

接下来,为每种原始标签类都定义根类的具体子类。所以可以将Figure类拆分成两个类,Circle和Rectangle类:

abstract class Figure {    abstract double area();}class Circle extends Figure {    final double radius;    Circle(double radius) {        this.radius = radius;    }    double area() {         return Math.PI *(radius * radius);    }}class Rectangle extends Figure {    final double length;    final double width;    Rectangle(double length,double width) {        this.length = length;        this.width = width;    }    double area() {        return length * width;    }}

这样的层析结构更加清晰,而且易于扩展:

class Square extends Rectangle {    Square(double side) {        super(side,side);    }}

第21条:用函数对象表示策略

考虑下面的类:

class StringLengthComparator {    public int compare(String s1,String s2) {        return s1.length() - s2.length();    }}

该类包含一个带有两个参数的方法,如果第一个字符串参数的长度比第二个长,就返回正数,相等就返回0,短就返回负数。该类实例的引用就可以作为一个函数指针,即该类的实例是用于字符串比较操作的具体策略。

这种策略类没有状态,即不包含域,所以没得每一个实例在功能上都是等价的,那么可以考虑使用单例类:

class StringLengthComparator {    private StringLengthComparator() {    }    public static final StringLengthComparator INSTANCE = new StringLengthComparator();    public int compare(String s1, String s2) {        return s1.length() - s2.length();    }} 

或者这样实现单例:

public StringLengthComparator {    private StringLengthComparator(){    }    private static StringLengthComparator singleton;     public static StringLengthComparator newInstance() {        if(singleton == null) {            syncronized(StringLengthComparator.class) {                if(singleton == null) {                    singleton = new StringLengthComparator();                }            }        }        return singleton;    }    public int compare(String s1 ,String s2) {        return s1.length() - s2.length();    }}

当然为了扩展比较的类型,还需要定义一个策略接口:

public interface Comparator<T> {    public int compare(T t1,T t2);}

用以比较其他类型的数据。

class StringLengthComparator implements Comparator<String> {    ...}

具体的策略类一般使用匿名类声明:

Arrays.sort(stringArray, new Comparator<String>() {    public int compare(String s1,String s2) {        return s1.length() - s2.length();    }});

以这种方式使用匿名类时,每次执行调用的时候会创建一个新的实例,那么可以把这个匿名类改成公有的静态域:

private static final Comparator<String> COMPARATOR = new Comparator<String>() {    public int compare(String s1, String s2) {        return s1.length() - s2.length();    }}Arrays.sort(stringArray, COMPARATOR);...

总结:函数指针的主要用途就是实现策略模式。为了在Java中实现这种模式,要声明一个接口来标识策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体的策略制备食用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,使其类型为该策略接口。


第22条:优先考虑静态成员类

首先说内部类,内部类是指被定义在一个类内部的类,它的作用就是为它的外部类提供服务。内部类分为四种:

  • 静态成员类

  • 非静态成员类

  • 匿名类

  • 局部类


本条目将分析何时该使用哪一种内部类,以及原因。


1、静态成员类:

这是一种最简单的内部类,知识碰巧被声明在另一个类的内部而已。他可以访问外围类的所有成员(包括私有的成员)。静态成员类就是外围类的一个静态成员,它与其他静态成员一样也遵守同样的额可访问性规则——即如果它被声明为私有的,那么只有外围类可以访问。

静态成员类的常见用法是作为共有类的辅助类,仅当与它的外部类一起使用时才有意义。举个栗子,考虑一个枚举类,它描述了计算器的各种操作。那么Operation枚举类就应该是Calculator类的公有静态类。然后,客户端就可以使用Calculator.Operation.PLUS和Calculator.Operation.MINUS来引用操作。

私有静态成员类在Android中的一个普遍用法是在BaseAdapter中建立一个私有的静态内部类ViewHolder,以绑定每一个item省的组件。由于ViewHolder只是声明了每个Item上的组件,并没有调用BaseAdapter中的任何方法,所以使用非静态的成员类就没有必要了(原因见下面的2、非静态内部类),当然如果漏掉了static,变成了非静态内部类,也可以执行,但每一个ViewHolder中就包含了一个BaseAdapter的引用,这会很占用空间和时间。

2、非静态成员类
在语法上,非静态成员类与静态成员类的唯一区别是,后者有static修饰符。但是在作用上有很大不同。非静态成员类的每个实例都与外围类的一个外围实例相关联,即非静态成员类的每个实例都隐式地持有一个外围类的引用,所以,非静态内部类可以调用外围类的任何方法。而且,非静态内部类的实例必须依赖于外围类的实例而存在,脱离外围类,非静态内部类的实例是不可能单独存在的。

当在外围类的某个方法中调用非静态内部类的的构造方法时,它们的关系就被建立了,而且不可被修改了。

非静态内部类的常用法是定义一个Adapter,即允许外部类的实例被看成是另一个不相关的类的实例。举个栗子,像Set、List这种集合的实现往往使用非静态内部类实现他们的迭代器:

public class MySet<E> extends AbstractSet<E> {    public Iterator<E> iterator() {        return new MyIterator();    }    private class MyIterator implements Iterator<E> {        ...    }}

如果声明的成员类不要求访问外围实例,那么就要把该内部类用static修饰,即成为静态成员类。如果省略了static,那么每一个内部类的实例都包含一个指向外围类对象的引用,保存这份引用需要时间和空间,并且当外围类需要被回收的时候,会造成还有引用指向该外围类而导致该外围类不会被马上回收,可能会造成内存泄漏。

3、匿名类

匿名类没名字,也不是外围类的成员,它在使用的时候才被声明和实例化,仅当匿名类出现在非静态的环境中时,才会引用外围实例。

匿名类的常用法是作为函数对象(见21条)。利用匿名的Comparator实例(确切的说是实现了Comparator接口的一个类对象的实例,该实例没有名字),对一组字符串对象进行长度的比较。

匿名类的另一种常见用法是创建过程对象。如Runable、Thread、TimerTask等。

4、局部类

布局类很少使用。它可以在任何可以声明局部变量的地方声明。它需要遵守局部变量的语法规则。由于使用的少(我是没用过),就一带而过了。

总结:
如果一个类需要在某个方法的内部是可见的、或者该类太长了,不适合放在方法内部,就应该考虑使用成员类。如果每一个成员类的实例都需要一个纸箱外围类的引用,就需要把该成员类定义成非静态的,否则就定义成静态的;加入这个内部类只属于一个方法内部,如果你只需要在一个地方建立实例,而且已经有了一个类可以说明这个类的特征,就应该使用匿名内部类;否则,就做成局部类。

0 0