问题
要区分设计良好的模块和设计不好的模块,最重要的因素在于,这个模块对于外部其他模块而言,是否隐藏其内部数据和其他细节。设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰的隔离起来,模块之间只通过它们的API进行通信,那么,在设计类和成员时有怎样的设计原则?
解决
设计类和成员有这样几个基本原则:
总结
总之,在设计类和成员时,应该尽可能的降低可访问性,除了公有静态final域的特殊情形之外,公有类都不应该包含公有域,并且要确保公有的静态final域所引用的对象是不可变的。
问题
有这样一个反例:
class Point {
public double x;
public double y;
}
复制代码
如上这样的类绝不应该声名为public,因为一旦声名为了public,该类中所有的数据就全部暴露出来,并且无法改变它的数据表示法,也无法强加任何约束条件,当被访问的时候,无法采取任何辅助措施,这么多问题,归结原因就是因为如果类声明不当,那么可能会将整个数据域全部暴露给客户端。虽然,对于可变类来说,应该用包含私有域和仅有设置方法的类代替:
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; }
}
复制代码
那么,对类中的数据域的访问级别应该如何设计?
解决
结论
公有类永远都不应该暴露可变的域,有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论这个类的域是可变的还是不可变的。
问题
不可变类是其实例不能被修改的类,没有实例中所包含的数据域,在实例被创建的时候被初始化,且在实例的生命周期中不能被修改。JAVA中有许多不可变类,如String,值的基本包装类型,BigInteger和BigDecimal等,不可变类是线程安全的。不可变有很多优点,那么设计不可变类的原则有哪些?
解决
设计不可变类有以下几条规则:
示例
例如,String不可变类的具体实现为:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
....
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
...
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
...
}
复制代码
如上代码所示,可以观察到以下设计细节:
结论
不可变类有很多好处,因此合适的适用场景下,可以考虑将类设计生不可变类,并遵守不可变类的设计原则。
问题
当通过子类继承父类并不是代码重用的最好手段,有这样的缺点:1. 与方法调用不同的是,继承打破封装性。子类依赖于父类,如果父类的具体实现细节改变,子类也会跟着相应改变。除非父类就是专门为扩展而设计的,并且有良好的文档说明;2. 父类方法中的”自用性“问题,导致的子类方法逻辑出错,比如统计HashSet自创建以来插入了多少个元素,需要覆盖add()方法和addAll()方法:
public class TestHashSet<E> extends HashSet<E> {
private int count = 0;
public TestHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
count++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
count += c.size();
return super.addAll(c);
}
public int getCount() {
return count;
}
public static void main(String[] args) {
TestHashSet<String> hashSet = new TestHashSet<String>(16, 0.75f);
hashSet.addAll(Arrays.asList(new String[]{"1","2","3"}));
System.out.println(hashSet.getCount());
}
}
复制代码
按照预想的会打印输出3,但实际上打印输出6。这是因为,addAll()方法内部实现调用了add()方法,因此总共的次数就是3+3=6。这种情况就是父类方法中”自用性“导致的。那么,针对由继承带来的问题应该如何解决?
解决
针对继承带来的问题,可以采用复合的方式进行解决,即不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。因此现有类变成了一个新类的一个组件,新类中的每个实例方法就可以调用被包含的类的实例方法,并返回相应的结果,这称之为转发。
采用复合/转发的方式重写上面的TestHash,包含了两个部分:新类本身以及被包含的转发类:
// 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,而是扩展了他的转发类,而在转发类内部,现有Set类是作为它的一个数据域存在的,转发类实现了Set接口,这样它就包括了现有类的基本操作。每个转发动作都直接调用现有类的相应方法并返回相应结果。这样就将信赖于Set的实现细节排除在包裹类之外。有的时候,复合和转发的结合被错误的称为"委托(delegation)"。从技术的角度来说,这不是委托,除非包装对象把自身传递给被包装的对象。
什么时候使用继承?
只有当子类真正是超类的子类型(subtype)时,才适合用继承。对于两个类A和B,只有当两者之间确实存在"is-a"的关系的时候,类B才应该扩展A。如果打算让类B扩展类A,就应该确定一个问题:B确实也是A吗?如果不能确定答案是肯定的,那么B就不应该扩展A。如果答案是否定的,通常情况下B应该包含A的一个私有实例,并且暴露一个较小的、较简单的API:A本质上不是B的一部分,只是它的实现细节而已(使用API的客户端无需知道)。
总结
简而言之,继承的功能非常强大,但是也存在诸多问题,因为它违反了封装原则。只有当子类和超类之间确实存在子类型的关系时,使用继承才是恰当的。即使如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性。为了避免这种情况,可以使用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更强大。
问题
之前阐述了贸然的将不是为了继承而设计的类进行继承,而实现子类化,是多么的危险,那么,在实际开发中,针对继承而设计的类怎样的处理才算是安全可靠的?
解决
结论
问题
有些语言支持函数指针、代理、lambda表达式,或者支持类似的机制,允许程序把”调用特殊函数的能力”储存起来并传递这种能力。最常用的例子就是比较函数,通过传入不同的比较策略会得到不同的比较结果,这也正是策略模式的一个例子。可是Java没有提供函数指针。
解决方案
Java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象上的某个操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象上的操作。如果一个类仅仅导出这样的一个方法,它的实例上就等同于一个指向该方法的指针。这样的实例被称为函数对象。考虑这样一个类:
class StringLengthComparator {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
复制代码
在这里,指向StringLengthComparator对象的引用可以被当作是一个指向该对象内部比较器compare的“函数指针”,可以在任意一对字符串上被调用,StringLengthComparator实例是用于比较字符串比较操作的具体策略。对于这种具体策略类,它的所有实例在功能上是相互等价的,所以根据前面的原则,将它作成是Singleton是非常合适的:
class StringLengthComparator {
private StringLengthComparator() {}
public static final StringLengthComparator
INSTANCE = new StringLengthComparator();
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
复制代码
但是,用这述这种方法有个问题,就是规定了参数的类型,这样就无法传递任何其他的比较策略。相反,对于这种情况,应该定义一个Comparator接口,并修改StringLengthComparator来实现这个接口。换句话说,在设计具体的策略类时,还需要定义一个策略接口:
// Strategy interface
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();
}
});
复制代码
这里存在一个问题,就是在每次执行调用的时候都会创建一个新的实例。如果它被重复执行,那就应该考虑将函数对象存储到一个私有的静态final域里并重用它。这样做的另一个好处就是为这个函数对象取一个有意义的声明。
因为策略接口被用做所有具体策略实例的类型,所以我们并不需要为了导出具体策略而把具体策略类做成公有的。可以导出公有的静态域或者静态工厂方法,其类型是策略接口,具体的策略类可以是宿主类的私有嵌套类:
class Host {
private static class StrlenCmp implements Comparator<String>, Serializable {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
// Returned comparator is serializable
public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrlenCmp();
}
复制代码
结论
问题
嵌套类(nested class)是指被定义在另一个类的内部的类。嵌套类存在的目的应该是为它的外围类(enclosing class)提供服务。如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类(top-level class)。嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class)。除了第一种之外,其他三种都被称为内部类(inner class)。那么,在什么情况下使用应该使用哪种嵌套类才是合适的?
解决方案
静态成员类
静态成员类是最简单的一种嵌套类。最好把它看作是普通的类,只是碰巧被声明在另一个类的内部而已,它可以访问外围类的所有成员,包括那些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。如果它被声明为私有的,它就只能在外围类的内部才可以被访问。
静态成员类的一种常见的用法就是作为公有的辅助类,仅当它的外部类一起使用时才有意义。例如,一个描述了计算器支持的各种操作的枚举。Operation枚举应该是Calculator类的公有静态成员类,然后使用Calculator类的客户端就可以用诸如Calculator.Operation.PLUS这样的名称来引用这些操作。
私有静态成员类的一种常见用法用来代表外围类所代表的对象的组件。例如,考虑一个Map实例,他把键和值关联起来。许多Map实现的内部都有一个Entry对象,对应于map中的每个键值对。虽然每个entry都与一个map关联,但是entry上的方法并不需要访问该map。因此,使用非静态成员来表示entry是很浪费的:private修饰的静态成员类是最佳的选择。如果不小心漏掉了entry声明中的static修饰符,该map依然可以工作,但是每个entry中将会包含一个指向该map的引用,这样就浪费了空间和时间。
非静态成员类
从语法上讲,静态成员类和非静态成员类之间唯一的区别是,静态成员类的声明中包含了修饰符static。尽管他们的语法非常的相似,但是这两种嵌套类有很大的不同。非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this获得外围实例的引用。如果嵌套类的实例可以在外围类的实例之外独立存在,这个嵌套类就必须是静态成员类,在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
当非静态成员类的实例被创建的时候,它和外围实例之间的关联关系也随之建立起来;而且,这种关联关系以后也不能被修改。通常情况下,当在外围类的某个实例方法的内部调用了非静态成员类的构造器时,这种管理就自动建立起来。使用表达式enclosingInstance.new MemberClass(args)来手工建立这种关系也是有可能的,但是很少使用。
非静态成员类常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例。例如,Map接口的实现往往使用非静态成员类来实现它们的集合视图(collection view),这些集合视图是由Map的keySet、entrySet和Values方法返回的。同样地,诸如Set和List这种集合接口的实现往往也是用非静态成员类来实现他们的迭代器(iterator):
public class MySet<E> extends AbstractSet<E>
{
public Iterator<E> iterator(){
return new MyIterator();
}
private class MyIterator implements Iterator<E>{
}
}
复制代码
静态成员类 VS 非静态成员类
匿名类
匿名类没有类名,它不是外围类的一个成员,并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态的环境中时,它才有外围实例。但是即使它们出现在静态的环境中,也不可能拥有任何静态成员。
匿名类的适用性受到诸多的限制。除了在它们被声明的时候之外,是无法将它们实例化的,你不能执行instanceof测试。你无法声明一个匿名类来实现多个接口,或者扩展一个类,并同时扩展类和实现接口。由于匿名类出现在表达式当中,它们必须保持简短——大约10行或者更少些——否则会影响程序的可读性。
匿名类多用于表示具体策略的函数对象,比如Arrays.sort()方法中定义的比较器Comparator,还可用于创建Thread时的Runnable等。
局部类
局部类是四种嵌套类中用的最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。局部类与其他三种嵌套类中的每一种都有一些共同的属性。与成员类一样,局部类有名字,可以被重复使用。与匿名类一样,只有当局部类实在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员。与匿名类一样,它们必须简短以便不会影响到可读性。
总结
简而言之,共有四种不同的嵌套类,每一种都有自己的用途。
原文:https://www.cnblogs.com/aishangJava/p/13682219.html