第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、局部类
布局类很少使用。它可以在任何可以声明局部变量的地方声明。它需要遵守局部变量的语法规则。由于使用的少(我是没用过),就一带而过了。
总结:
如果一个类需要在某个方法的内部是可见的、或者该类太长了,不适合放在方法内部,就应该考虑使用成员类。如果每一个成员类的实例都需要一个纸箱外围类的引用,就需要把该成员类定义成非静态的,否则就定义成静态的;加入这个内部类只属于一个方法内部,如果你只需要在一个地方建立实例,而且已经有了一个类可以说明这个类的特征,就应该使用匿名内部类;否则,就做成局部类。