关闭

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

标签: 类访问权限publicprotectedprivate
457人阅读 评论(0) 收藏 举报
分类:

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


简介

    区别设计良好的模块和设计不好的模块,最重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏了其内部数据和其他细节。设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后,模块之间只通过它们的API进行通信,一个模块不需要知道其他模块内部的工作情况。这个概念被称为信息隐藏(information hiding)封装(encapsulation),是软件设计的基本原则之一。

优点

    信息隐藏之所以非常重要有很多原因,其中大多数理由都源于这样的一个事实:它可以有效地接触组成系统的个模块之间的耦合关系,使得这些模块可以独立地开发,测试,优化,使用,理解和修改。这样可以加快系统开发的速度,因为这些模块可以并行开发。也减轻了维护的负担,因为程序员可以更快的理解这些模块,并且在调试他们的时候可以不影响其他的模块。虽然信息隐藏本身无论是对内还是对外,都不会带来更好的性能,但是它可以有效地调节性能:一旦完成一个系统,并通过剖析确定了哪些模块影响了系统的性能,那些模块就可以被进一步的优化,而不会影响到其他模块的正确性。信息隐藏提高了软件的可重用性,因为模块之间并不紧密相连,除了开发这些模块所使用的环境之外,它们在其他的环境中往往也很有用。最后,信息隐藏也降低了重构大型系统的风险,因为即使整个系统不可用,但是这些独立的模块却还是可能可用的。

    Java程序设计语言提供了许多机制来协助信息隐藏。访问控制(access control)机制决定了类、接口和成员的可访问性。实体的可访问性是由该实体声明所在的位置,以及该实体声明中所出现的访问修饰符(private、protected和public)共同决定的。正确的使用这些修饰符对于实现信息隐藏是非常关键的。

    第一规则很简单:尽可能地使每个类或者成员不被外界访问。换句话说,应该使用与你正在编写的软件的对应功能相一致的、尽可能最小的访问级别。也就是能不暴露就不暴露。

    对于顶层的(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的(package-private)和公有的(public)。通过把类或者接口做成包私有级的,实际上成了这个包的实现的一部分,而不是该包导出的API的一部分,在以后的发行版本中,可以对它进行修改、替换、或者删除,而无需担心会影响到现有的客户端程序。如果你把它做成了公有的,那就有责任永远支持它,以保证兼容性。

    如果一个包级私有的顶层类(或者接口)只是在某一个类的内部被用到,那就应该考虑使用它成为唯一使用它的那个类的私有嵌套类。这样可以将它的可访问范围从包中的所有类缩小到了使用它的那个类。然而,降低不必要公有类的可访问性,比降低包私有级的顶层类重要的多:因为公有类是包的API的一部分,而包级私有的顶层类则已经是这个包的实现的一部分了。API是要对外界暴露的,所以调节这些的隐藏性更加的重要。

    对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别,下面按照可访问性的递增顺序罗列出来:

  • 私有的(private): 只有在声明该成员的顶层类内部才可以访问这个成员。
  • 包级私有(package-private): 声明该成员的包内部的任何类都可以访问这个成员。它一般被称为缺省(default)访问级别,如果没有为成员指定访问修饰符,就采用这个访问级别。
  • 受保护的(protected): 声明该成员的类的子类可以访问,声明该成员的包内部的任何类也可以访问。
  • 公有的(public): 在任何地方都可以访问。

关于访问级别的建议

    仔细设计类的API之后,可能会觉得应该把除了API之外的其他成员全部变为私有的。其实,只有当同一个包内的另一个类真正需要访问一个成员的时候,你才应该删除private修饰符,使该成员变成包级私有的。

    对于共有类的成员,当访问级别从包级私有变成保护级别时,会大大增强可访问性。受保护的成员是类的导出的API的一部分,必须永远得到支持。导出的类的受保护成员也代表了该类对于某个实现细节的公开承诺,受保护的成员应该尽量少用。

    有一条规则限制了降低方法的可访问性的能力。如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。这样可以确保任何可使用超类的实例的地方也都可以使用子类的实力。如果你违反了这条规则,那么当你试图编译该子类的时候,编译器会产生一条错误消息。这条规则有种特殊的情况:如果一个类实现了一个几口,那么接口中所有的类方法在这个类中也都必须声明称为公有的。之所以如此,是因为接口中的所有方法都隐藏着公有访问级别。也就是说,重写父类方法的时候,不能将访问权限扩大,只能将访问权限缩小或者不变。父类的public方法可以在子类中表现出protected的访问权限m,但是父类的protected不能在子类中表现出public的权限。

    为了方便测试,可以试着使类、接口或者成员变量更加容易访问。这么做在一定程度上来说是好的。为了测试而将一个公有类的私有成员变成包级私有,这还可以接受,但是再进一步的扩大访问权限,就有点不合适了。可以替代的让测试作为被测试部分的包的一部分来运行,可以方便的测试protected访问级别的元素,还不至于破坏该模块的封装性。

    实例域绝不能是公有的。如果域是非final的,或者是一个指向可变对象的final引用,那么一旦使这个域称为公有的,就放弃了对存储在这个域中的值进行限制的能力;这意味着,你也放弃了强制这个域不可变的能力。同时,当这个域被修改的时候,你也失去了对它采取任何行动的能力。因此,包含公有可变域的类并不是线程安全的。

    同样的建议也适用于静态域,只是有一种例外情况。假设常量构成了类提供的整个抽象中的一部分,可以通过公有的静态final域来暴露这些常量。按照惯例,这种域的名称由大写字母组成,单词之间用下划线分开。很重要的一点就是,这些域要么包含基本类型的值,要么包含指向不可变对象的引用。如果final域包含可变对象的引用,它便失去了final域的特性,虽然本身引用不可修改,但是所指向的对象却可以被修改,这个后果比较严重。

    长度非0的数组总是可变的,因此类具有公有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的。如果类具有这样的域或者访问方法,客户端将能够修改数组中的内容。这是安全漏洞的一个常见的根源:

public static final Thing[] VALUES = { ... };

    要注意许多IDE会产生返回指向私有数组域的引用的访问方法。修正这个问题有两种方法,一种是使公邮数组变成私有的,并增加一个公有的不可变列表:

private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

    关于Collections.java的unmodifiableList方法,在源码中是这样的:

public static <T> List<T> unmodifiableList(List<? extends T> list) {
    return (list instanceof RandomAccess ? 
        new UnmodifiableRandomAccessList<> (list) :
        new UnmodifiableList<>(list));
}

    在上面提到了RandomAccess.java这个接口,这个接口在官方上是这么解释的:

    Marker interface used by List implementations to indicate that they
    support fast (generally constant time) random access. The primary purpose 
    of this interface is to allow generic algorithms to alter their behavior
    to provide good performance when applied to either random or sequential
    access lists.

    The best algorithms for manipulating random access lists (such as
    ArrayList) can produce quadratic behavior when applied to sequential
    access lists (such as LinkedList). Generic list algorithms are encouraged
    to check whether the given list is an instanceof this interface before
    applying an algorithm that would provide poor performance if it were
    applied to a sequential access list, and to alter their behavior if
    necessary to guarantee acceptable performance.

    It is recognized that the distinction between random and sequential
    access is often fuzzy. For example, some List implementations provide
    asymptotically linear access times if they get huge, but constant access
    times in practice. Such a List implementation should generally implement
    this interface. As a rule of thumb, a List implementation should
    implement this interface if, for typical instances of the class, this
    loop:

     for (int i=0, n=list.size(); i < n; i++)
         list.get(i);

    runs faster than this loop:
         for (Iterator i=list.iterator(); i.hasNext(); )
             i.next();

    This interface is a member of the Java Collections Framework.

    RandomAccess接口是Collections框架的一部分,用来标识List及其子类的类实例支持随机快速访问的。这个接口最初的目的是用来允许泛型算法改变他们具体的行为,来提供更高的性能,当这些算法被用于随机访问的list(例如ArrayList)或者顺序访问的list(LinkedList)。当用于操纵随机访问List的访问算法用于顺序访问List的时候,会造成二次访问,这样效率就会很底下,因此进行访问的算法内部会进行检查当前访问的list是否满足list instanceof RandomAccess这样一个条件,以便来选择更高效的访问算法。

    UnmodifiableList类是一个List的包装类,从名字上看的出是不可修改的List,在这个的内部只提供get类的方法,一切set的方法都被覆盖且抛出异常,这就保证了不可修改这个性质。下面是实现的代码:

/**
 * @serial include
 */
static class UnmodifiableList<E> extends UnmodifiableCollection<E>
                              implements List<E> {
    private static final long serialVersionUID = -283967356065247728L;

    final List<? extends E> list;

    UnmodifiableList(List<? extends E> list) {
        super(list);
        this.list = list;
    }

    public boolean equals(Object o) {return o == this || list.equals(o);}
    public int hashCode()           {return list.hashCode();}

    public E get(int index) {return list.get(index);}
    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }
    public int indexOf(Object o)            {return list.indexOf(o);}
    public int lastIndexOf(Object o)        {return list.lastIndexOf(o);}
    public boolean addAll(int index, Collection<? extends E> c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void replaceAll(UnaryOperator<E> operator) {
        throw new UnsupportedOperationException();
    }
    @Override
    public void sort(Comparator<? super E> c) {
        throw new UnsupportedOperationException();
    }

    public ListIterator<E> listIterator()   {return listIterator(0);}

    public ListIterator<E> listIterator(final int index) {
        return new ListIterator<E>() {
            private final ListIterator<? extends E> i
                = list.listIterator(index);

            public boolean hasNext()     {return i.hasNext();}
            public E next()              {return i.next();}
            public boolean hasPrevious() {return i.hasPrevious();}
            public E previous()          {return i.previous();}
            public int nextIndex()       {return i.nextIndex();}
            public int previousIndex()   {return i.previousIndex();}

            public void remove() {
                throw new UnsupportedOperationException();
            }
            public void set(E e) {
                throw new UnsupportedOperationException();
            }
            public void add(E e) {
                throw new UnsupportedOperationException();
            }

            @Override
            public void forEachRemaining(Consumer<? super E> action) {
                i.forEachRemaining(action);
            }
        };
    }

    public List<E> subList(int fromIndex, int toIndex) {
        return new UnmodifiableList<>(list.subList(fromIndex, toIndex));
    }

    /**
     * UnmodifiableRandomAccessList instances are serialized as
     * UnmodifiableList instances to allow them to be deserialized
     * in pre-1.4 JREs (which do not have UnmodifiableRandomAccessList).
     * This method inverts the transformation.  As a beneficial
     * side-effect, it also grafts the RandomAccess marker onto
     * UnmodifiableList instances that were serialized in pre-1.4 JREs.
     *
     * Note: Unfortunately, UnmodifiableRandomAccessList instances
     * serialized in 1.4.1 and deserialized in 1.4 will become
     * UnmodifiableList instances, as this method was missing in 1.4.
     * 在类的最后对Serializable接口进行了处理,这个接口是在UnmodifiableCollection
     * 类中实现的,这里对从文件流中读取出的实例结果进行了替换。
     */
    private Object readResolve() {
        return (list instanceof RandomAccess
                ? new UnmodifiableRandomAccessList<>(list)
                : this);
    }
}

    UnmodifiableRandomAccessList类和UnmodifiableList基本没有什么区别,唯一的区别就是实现了RandomAccess这个接口,标识这个类型的List能用随机访问的算法来进行访问。

    另外一种方法就是,可以使数组变成私有的,并添加一个公邮方法,返回私有数组的一个备份:

private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:131951次
    • 积分:2102
    • 等级:
    • 排名:第18089名
    • 原创:72篇
    • 转载:8篇
    • 译文:0篇
    • 评论:35条