Java程序设计 2 封装

封装

所有优秀的作者——包括那些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是重构(refactoring)的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。

包内包含一组类,它们被组织在一个单独的namespace下
例如,标准Java发布中有一个工具库,它被组织在java.util命名空间下。java.util中含有一个类,叫做ArrayList。使用ArrayList的一种方式是用其全名java.util.ArrayList

public class FullQualification {
    public static void main(String[] args) {
        java.util.ArrayList list = new java.util.ArrayList();
    }
}

这种方式使得程序冗长乏味,因此你可以换一种方式,使用 import 关键字。如果需要导入某个类,就需要在 import 语句中声明:

import java.util.ArrayList;

public class SingleImport {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
    }
}
import java.util.*

一个 Java 源代码文件称为一个编译单元(compilation unit)(有时也称翻译单元(translation unit))。每个编译单元的文件名后缀必须是 .java。在编译单元中可以有一个 public 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 .java)。每个编译单元中只能有一个 public 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 public 类,此时它们为主 public 类提供“支持”类 。

代码组织

当编译一个 .java 文件时, .java 文件的每个类都会有一个输出文件。每个输出的文件名和 .java 文件中每个类的类名相同,只是后缀名是 .class。因此,在编译少量的 .java 文件后,会得到大量的 .class 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为“obj”文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 .class 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 jar 文档生成器)。Java 解释器负责查找、加载和解释这些文件。

使用 import 改变行为

Java 没有 C 的条件编译(conditional compilation)功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。

但是,条件编译还有其他的用途。调试是一个很常见的用途,调试功能在开发过程中是开启的,在发布的产品中是禁用的。可以通过改变导入的 package 来实现这一目的,修改的方法是将程序中的代码从调试版改为发布版。这个技术可用于任何种类的条件代码。

使用包的忠告

当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 package 可能会有点不顺,因为除非遵守“包名对应目录路径”的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 package 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。

注意,编译过的代码通常位于与源代码的不同目录中。这是很多工程的标准,而且集成开发环境(IDE)通常会自动为我们做这些。必须保证 JVM 通过 CLASSPATH 能找到编译后的代码。

访问权限修饰符

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

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

包访问权限

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

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

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

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

public: 接口访问权限

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

package com.eintr;

import com.eintr.dessert.Cookie;
public class App 
{
  public static void main( String[] args )
  {
    Cookie c = new Cookie();
    // c.f(); //引用不到
    c.f(2);
    System.out.println( "Hello World!" );
  }
}
package com.eintr;

import com.eintr.dessert.Cookie;
public class App 
{
  public static void main( String[] args )
  {
    Cookie c = new Cookie();
    // c.f(); //引用不到
    c.f(2);
    System.out.println( "Hello World!" );
  }
}

默认包

private: 你无法访问

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

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

private 还可以用于控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。

protected: 继承访问权限

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

package com.eintr.dessert;

public class Cookie {
  private int a;
  public int b;
  protected int c;

  {
    a = 1;
    b = 2;
    c = 3;
  }
  public Cookie() {
    System.out.println("Cookie constructor");
  }

  void f() {
    System.out.println("f()");
  }

  public void f(int i) {
    System.out.printf("f(i+a) = %d\n", i+a);
  }
}
package com.eintr.food;

public class Cake extends Cookie {
  public Cake() {
    // a = 4; 无法访问
    b = 4;
    c = 5;
    System.out.println("Cake()");
  }

}
import com.eintr.food.Cake;

public class App 
{
  public static void main( String[] args )
  {
    Cake c = new Cake();
    c.f(2);
    System.out.println( "Hello World!" );
  }
}

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

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

接口和实现

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

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

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

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

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 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值