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、局部类
布局类很少使用。它可以在任何可以声明局部变量的地方声明。它需要遵守局部变量的语法规则。由于使用的少(我是没用过),就一带而过了。
总结:
如果一个类需要在某个方法的内部是可见的、或者该类太长了,不适合放在方法内部,就应该考虑使用成员类。如果每一个成员类的实例都需要一个纸箱外围类的引用,就需要把该成员类定义成非静态的,否则就定义成静态的;加入这个内部类只属于一个方法内部,如果你只需要在一个地方建立实例,而且已经有了一个类可以说明这个类的特征,就应该使用匿名内部类;否则,就做成局部类。
- Effective Java读书笔记——第四章 类和接口
- [Effective Java]第四章 类和接口
- effective java 读书笔记---第四章类与接口
- Effective Java读书笔记(第4章-类和接口)
- Effective Java读书笔记、感悟——3.类和接口(一)
- Effective Java读书笔记(4 类和接口)
- 【读书笔记】《Effective Java》(3)-- 类和接口
- 【effective Java读书笔记】类和接口(一)
- 【effective Java读书笔记】类和接口(二)
- Effective Java 读书笔记(三):类和接口
- effective STL 读书笔记——第四章:迭代器
- Effective Java读书笔记(一):类、接口、对象
- Effective Java读书笔记-接口优于抽象类
- Effective Java读书笔记——第六章 枚举和注解
- Effective Java——类和接口(上)
- Effective Java——类和接口(下)
- effective java(类和接口)
- Effective Java:类和接口
- Swift UIButton的使用详解
- 【python】获取实际内存数据pss--total
- JAVA操作PGP非对称加密实践
- 简单实现Shiro单点登录(自定义Token令牌)
- 机器学习参考图书
- Effective Java读书笔记——第四章 类和接口
- hdu 5790 prefix 主席树在线维护区间不同数的个数
- emacs for Mac命令
- Java实现简单的数据结构(一)
- smmu学习笔记之bus_set_iommu
- Python学习笔记13
- this
- JavaScript 随机数
- 【51nod1363】最小公倍数之和