七、封装(2)

本章概要

  • 访问权限修饰符
    • 包访问权限
    • public:接口访问权限
    • 默认包
    • private:你无法访问
    • protected:继承访问权限
    • 包访问权限 Vs Public 构造器
  • 接口和实现
  • 类访问权限

访问权限修饰符

Java 访问权限修饰符 publicprotectedprivate 位于定义的类名,属性名和方法名之前。每个访问权限修饰符只能控制它所修饰的对象。

如果不提供访问修饰符,就意味着"包访问权限"。所以无论如何,万物都有某种形式的访问控制权。接下来的几节中,你将学习各种类型的访问权限。

包访问权限

本章之前的所有示例要么使用 public 访问修饰符,要么就没使用修饰符(默认访问权限(default access))。默认访问权限没有关键字,通常被称为_包访问权限(package access)_(有时也称为 friendly)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 private 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。

包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类赋予了它们包访问权限的成员相互访问的权限,所以你"拥有”了包内的程序代码。只能通过你所拥有的代码去访问你所拥有的其他代码,这样规定很有意义。构建包访问权限机制是将类聚集在包中的重要原因之一。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可能会将不应该对当前包中的类具有访问权限的类排除在包外。

类控制着哪些代码有权访问自己的成员。其他包中的代码不能一上来就说"嗨,我是 Bob 的朋友!",然后想看到 Bobprotected、包访问权限和 private 成员。取得对成员的访问权的唯一方式是:

  1. 使成员成为 public。那么无论是谁,无论在哪,都可以访问它。
  2. 赋予成员默认包访问权限,不用加任何访问修饰符,然后将其他类放在相同的包内。这样,其他类就可以访问该成员。
  3. 在"复用"这一章你将看到,继承的类既可以访问 public 成员,也可以访问 protected 成员(但不能访问 private 成员)。只有当两个类处于同一个包内,它才可以访问包访问权限的成员。但现在不用担心继承和 protected
  4. 提供访问器(accessor)和修改器(mutator)方法(有时也称为"get/set" 方法),从而读取和改变值。

public:接口访问权限

当你使用关键字 public,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 dessert 包:

// hiding/dessert/Cookie.java
// Creates a library
package hiding.dessert;

public class Cookie {
    public Cookie() {
        System.out.println("Cookie constructor");
    }
    
    void bite() {
        System.out.println("bite");
    }
}

记住,Cookie.java 文件产生的类文件必须位于名为 dessert 的子目录中,该子目录在 hiding (表明本书的"封装"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 .,Java 就不会查找当前目录。

现在,使用 Cookie 创建一个程序:

// hiding/Dinner.java
// Uses the library
import hiding.dessert.*;

public class Dinner {
    public static void main(String[] args) {
        Cookie x = new Cookie();
        // -x.bite(); // Can't access
    }
}

输出:

Cookie constructor

你可以创建一个 Cookie 对象,因为它构造器和类都是 public 的。(后面会看到更多 public 的概念)但是,在 Dinner.java 中无法访问到 Cookie 对象中的 bite() 方法,因为 bite() 只提供了包访问权限,因而在 dessert 包之外无法访问,编译器禁止你使用它。

默认包

你可能惊讶地发现,以下代码尽管看上去破坏了规则,但是仍然可以编译:

// hiding/Cake.java
// Accesses a class in a separate compilation unit
class Cake {
    public static void main(String[] args) {
        Pie x = new Pie();
        x.f();
    }
}

输出:

Pie.f()

同一目录下的第二个文件:

// hiding/Pie.java
// The other class
class Pie {
    void f() {
        System.out.println("Pie.f()");
    }
}

最初看上去这两个文件毫不相关,但在 Cake 中可以创建一个 Pie 对象并调用它的 f() 方法。(注意,你的 CLASSPATH 中一定得有 .,这样文件才能编译)通常会认为 Pie 和  f() 具有包访问权限,因此不能被 Cake 访问。它们的确具有包访问权限,这是部分正确。Cake.java 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。

private: 你无法访问

关键字 private 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 private 成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用 private,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。

默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。这样做很好,因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 public 供客户端程序员使用。所以,最初不常使用关键字 private,因为程序没有它也可以照常工作。然而,使用 private 是非常重要的,尤其是在多线程环境中。(在"并发编程"一章中将看到)。

以下是一个使用 private 的例子:

// hiding/IceCream.java
// Demonstrates "private" keyword

class Sundae {
    private Sundae() {}
    static Sundae makeASundae() {
        return new Sundae();
    }
}

public class IceCream {
    public static void main(String[] args) {
        //- Sundae x = new Sundae();
        Sundae x = Sundae.makeASundae();
    }
}

以上展示了 private 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 Sundae 对象,而必须调用 makeASundae() 方法创建对象。

任何可以肯定只是该类的"助手"方法,都可以声明为 private,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 private 确保了你拥有这种选择权。

对于类中的 private 属性也是一样。除非必须公开底层实现(这种情况很少见),否则就将属性声明为 private。然而,不能因为类中某个对象的引用是 private,就认为其他对象也无法拥有该对象的 public 引用(参见附录:对象传递和返回)。

protected: 继承访问权限

要理解 protected 的访问权限,我们在内容上需要作一点跳跃。首先,在介绍本书"复用"章节前,你不必真正理解本节的内容。但为了内容的完整性,这里作了简要介绍,举了个使用 protected 的例子。

关键字 protected 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样:

class Foo extends Bar {}

类定义的其他部分看起来是一样的。

如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 public 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 protectedprotected 也提供包访问权限,也就是说,相同包内的其他类可以访问 protected 元素。

回顾下先前的文件 Cookie.java,下面的类不能调用包访问权限的方法 bite()

// hiding/ChocolateChip.java
// Can't use package-access member from another package
import hiding.dessert.*;

public class ChocolateChip extends Cookie {
    public ChocolateChip() {
        System.out.println("ChocolateChip constructor");
    } 
    
    public void chomp() {
        //- bite(); // Can't access bite
    }
    
    public static void main(String[] args) {
        ChocolateChip x = new ChocolateChip();
        x.chomp();
    }
}

输出:

Cookie constructor
ChocolateChip constructor

如果类 Cookie 中存在一个方法 bite(),那么它的任何子类中都存在 bite() 方法。但是因为 bite() 具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为 public,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 Cookie 改成如下这样:

// hiding/cookie2/Cookie.java
package hiding.cookie2;

public class Cookie {
    public Cookie() {
        System.out.println("Cookie constructor");
    }
    
    protected void bite() {
        System.out.println("bite");
    }
}

这样,bite() 对于所有继承 Cookie 的类,都是可访问的:

// hiding/ChocolateChip2.java
import hiding.cookie2.*;

public class ChocolateChip2 extends Cookie {
    public ChocoalteChip2() {
        System.out.println("ChocolateChip2 constructor");
    }
    
    public void chomp() {
        bite(); // Protected method
    }
    
    public static void main(String[] args) {
        ChocolateChip2 x = new ChocolateChip2();
        x.chomp();
    }
}

输出:

Cookie constructor
ChocolateChip2 constructor
bite

尽管 bite() 也具有包访问权限,但它不是 public 的。

包访问权限 Vs Public 构造器

当你定义一个具有包访问权限的类时,你可以在类中定义一个 public 构造器,编译器不会报错:

// hiding/packageaccess/PublicConstructor.java
package hiding.packageaccess;

class PublicConstructor {
    public PublicConstructor() {}
}

有一个 Checkstyle 工具,你可以运行命令 gradlew hiding:checkstyleMain 使用它,它会指出这种写法是虚假的,而且从技术上来说是错误的。实际上你不能从包外访问到这个 public 构造器:

// hiding/CreatePackageAccessObject.java
// {WillNotCompile}
import hiding.packageaccess.*;

public class CreatePackageAcessObject {
    public static void main(String[] args) {
        new PublicConstructor();
    }
}

如果你编译下这个类,会得到编译错误信息:

CreatePackageAccessObject.java:6:error:
PublicConstructor is not public in hiding.packageaccess;
cannot be accessed from outside package
new PublicConstructor();
^
1 error

因此,在一个具有包访问权限的类中定义一个 public 的构造器并不能真的使这个构造器成为 public,在声明的时候就应该标记为编译时错误。

接口和实现

访问控制通常被称为_隐藏实现_(implementation hiding)。将数据和方法包装进类中并把具体实现隐藏被称作是_封装_(encapsulation)。其结果就是一个同时带有特征和行为的数据类型。

出于两个重要的原因,访问控制在数据类型内部划定了边界。第一个原因是确立客户端程序员可以使用和不能使用的边界。可以在结构中建立自己的内部机制而不必担心客户端程序员偶尔将内部实现作为他们可以使用的接口的一部分。

这直接引出了第二个原因:将接口与实现分离。如果在一组程序中使用接口,而客户端程序员只能向 public 接口发送消息的话,那么就可以自由地修改任何不是 public 的事物(例如包访问权限,protected,或 private 修饰的事物),却不会破坏客户端代码。

为了清晰起见,你可以采用一种创建类的风格:public 成员放在类的开头,接着是 protected 成员,包访问权限成员,最后是 private 成员。这么做的好处是类的使用者可以从头读起,首先会看到对他们而言最重要的部分(public 成员,因为可以从文件外访问它们),直到遇到非 public 成员时停止阅读,下面就是内部实现了:

// hiding/OrganizedByAccess.java

public class OrganizedByAccess {
    public void pub1() {/* ... */}
    public void pub2() {/* ... */}
    public void pub3() {/* ... */}
    private void priv1() {/* ... */}
    private void priv2() {/* ... */}
    private void priv3() {/* ... */}
    private int i;
    // ...
}

这么做只能是程序阅读起来稍微容易一些,因为实现和接口还是混合在一起。也就是说,你仍然能看到源代码——实现部分,因为它就在类中。另外,javadoc 提供的注释文档功能降低了程序代码的可读性对客户端程序员的重要性。将接口展现给类的使用者实际上是类浏览器的任务,类浏览器会展示所有可用的类,并告诉你如何使用它们(比如说哪些成员可用)。在 Java 中,JDK 文档起到了类浏览器的作用。

类访问权限

访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 public 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。

为了控制一个类的访问权限,修饰符必须出现在关键字 class 之前:

public class Widget {

如果你的类库名是 hiding,那么任何客户端程序员都可以通过如下声明访问 Widget

import hiding.Widget;

或者

import hiding.*;

这里有一些额外的限制:

  1. 每个编译单元(即每个文件)中只能有一个 public 类。这表示,每个编译单元有一个公共的接口用 public 类表示。该接口可以包含许多支持包访问权限的类。一旦一个编译单元中出现一个以上的 public 类,编译就会报错。
  2. public 类的名称必须与含有该编译单元的文件名相同,包括大小写。所以对于 Widget 来说,文件名必须是 Widget.java,不能是 widget.javaWIDGET.java。再次强调,如果名字不匹配,编译器会报错。
  3. 虽然不是很常见,但是编译单元内没有 public 类也是可能的。这时可以随意命名文件(尽管随意命名会让代码的阅读者和维护者感到困惑)。

如果是一个在 hiding 包中的类,只用来完成 Widgethiding 包下一些其他 public 类所要执行的任务,怎么设置它的访问权限呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 hiding 中的任何特定细节,那么把 public 关键字从类中去掉,给予它包访问权限,就可以了。

当你创建了一个包访问权限的类,把类中的属性声明为 private 仍然是有意义的——应该尽可能将所有属性都声明为 private,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。一个包访问权限的类只能被用于包内,除非强制将某些方法声明为 public,这种情况下,编译器会告诉你。

注意,类既不能是 private 的(这样除了该类自身,任何类都不能访问它),也不能是 protected 的。所以对于类的访问权限只有两种选择:包访问权限或者 public。为了防止类被外界访问,可以将所有的构造器声明为 private,这样只有你自己能创建对象(在类的 static 成员中):

// hiding/Lunch.java
// Demonstrates class access specifiers. Make a class
// effectively private with private constructors:

class Soup1 {
    private Soup1() {}
    
    public static Soup1 makeSoup() { // [1]
        return new Soup1();
    }
}

class Soup2 {
    private Soup2() {}
    
    private static Soup2 ps1 = new Soup2(); // [2]
    
    public static Soup2 access() {
        return ps1;
    }
    
    public void f() {}
}
// Only one public class allowed per file:
public class Lunch {
    void testPrivate() {
        // Can't do this! Private constructor:
        //- Soup1 soup = new Soup1();
    }
    
    void testStatic() {
        Soup1 soup = Soup1.makeSoup();
    }
    
    void testSingleton() {
        Soup2.access().f();
    }
}

可以像 [1] 那样通过 static 方法创建对象,也可以像 [2] 那样先创建一个静态对象,当用户需要访问它时返回对象的引用即可。

到目前为止,大部分的方法要么返回 void,要么返回基本类型,所以 [1] 处的定义乍看之下会有点困惑。方法名(makeSoup)前面的 Soup1 表明了方法返回的类型。到目前为止,这里经常是 void,即不返回任何东西。然而也可以返回对象的引用,就像这里一样。这个方法返回了对 Soup1 类对象的引用。

Soup1Soup2 展示了如何通过将你所有的构造器声明为 private 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 private,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 Soup1 中,有一个 static 方法,它的作用是创建一个新的 Soup1 对象并返回对象的引用。如果想要在返回引用之前在 Soup1 上做一些额外操作,或是记录创建了多少个 Soup1 对象(可以用来限制数量),这种做法是有用的。

Soup2 用到了所谓的_设计模式_(design pattern)。这种模式叫做_单例模式_(singleton),因为它只允许创建类的一个对象。Soup2 类的对象是作为 Soup2static private 成员而创建的,所以有且只有一个,你只能通过 public 修饰的 access() 方法访问到这个对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小熊猫呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值