小何与小乌的故事

« Linux主要文件学习笔记1 | Home | 看完了深入浅出C++第三章(MFC六大关键技术之模拟),thinking in java第六章的3/4! »

thinking in java 第五章 隐藏具体实现

By robinhesky | 01月 18, 2008

进行面向对象的设计时,一项基本的考虑是:如何将发生变化的东西与保持不变的东西分隔开

为解决这个问题,Java推出了“访问指示符”的概念,允许库创建者声明哪些东西是客户程序员可以使用的,哪些是不可使用的。这种访问控制的级别在“最大访问”和“最小访问”的范围之间,分别包括:public,protected,包访问权限(没有关键词),private。

1.库单元

我们用import关键字导入一个完整的库时,就会获得“包”(Package)。例如:
import java.util.*;

之所以要进行这样的导入,是为了提供一种特殊的机制,以便管理“命名空间”(Name Space)。我们所有类成员的名字相互间都会隔离起来。位于类A内的一个方法f()不会与位于类B内的、拥有相同“签名”(自变量列表)的f()发生冲突

若在一个文件的开头使用下述代码:
package mypackage;
那么package语句必须作为文件的第一个非注释语句出现。该语句的作用是指出这个编译单元属于名为mypackage的一个库的一部分。或者换句话说,它表明这个编译单元内的public类名位于mypackage这个名字的下面。如果其他人想使用这个名字,要么指出完整的名字,要么与mypackage联合使用import关键字(使用前面给出的选项)。注意根据Java包(封装)的约定,名字内的所有字母都应小写,甚至那些中间单词亦要如此。

2.创建独一无二的域名

由于一个包永远不会真的“封装”到单独一个文件里面,它可由多个.class文件构成,所以局面可能稍微有些混乱。为避免这个问题,最合理的一种做法就是将某个特定包使用的所有.class文件都置入单个目录里。也就是说,我们要利用操作系统的分级文件结构避免出现混乱局面。这正是Java所采取的方法。
它同时也解决了另两个问题:创建独一无二的包名以及找出那些可能深藏于目录结构某处的类。正如我们在第2章讲述的那样,为达到这个目的,需要将.class文件的位置路径编码到package的名字里。但根据约定,编译器强迫package名的第一部分是类创建者的因特网域名。由于因特网域名肯定是独一无二的(由InterNIC保证——注释②,它控制着域名的分配),所以假如按这一约定行事,package的名称就肯定不会重复,所以永远不会遇到名称冲突的问题。换句话说,除非将自己的域名转让给其他人,而且对方也按照相同的路径名编写Java代码,否则名字的冲突是永远不会出现的。

这个技巧的另一部分是将package名解析成自己机器上的一个目录。这样一来,Java程序运行并需要装载.class文件的时候(这是动态进行的,在程序需要创建属于那个类的一个对象,或者首次访问那个类的一个static成员时),它就可以找到.class文件驻留的那个目录。
Java解释器的工作程序如下:首先,它找到环境变量CLASSPATH(将Java或者具有Java解释能力的工具——如浏览器——安装到机器中时,通过操作系统进行设定)。CLASSPATH包含了一个或多个目录,它们作为一种特殊的“根”使用,从这里展开对.class文件的搜索。从那个根开始,解释器会寻找包名,并将每个点号(句点)替换成一个斜杠,从而生成从CLASSPATH根开始的一个路径名(所以package foo.bar.baz会变成foo\bar\baz或者foo/bar/baz;具体是正斜杠还是反斜杠由操作系统决定)。随后将它们连接到一起,成为CLASSPATH内的各个条目(入口)。以后搜索.class文件时,就可从这些地方开始查找与准备创建的类名对应的名字。此外,它也会搜索一些标准目录——这些目录与Java解释器驻留的地方有关。

对于如下的包名,编译器是这样做的:

package com.bruceeckel.util;

这两个文件都置于我自己系统的一个子目录中:
C:\DOC\JavaT\com\bruceeckel\util
若通过它往回走,就会发现包名com.bruceeckel.util,但路径的第一部分又是什么呢?这是由CLASSPATH环境变量决定的。在我的机器上,它是:
CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT
可以看出,CLASSPATH里能包含大量备用的搜索路径。然而,使用JAR文件时要注意一个问题:必须将JAR文件的名字置于类路径里,而不仅仅是它所在的路径。所以对一个名为grape.jar的JAR文件来说,我们的类路径需要包括:
CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar

3.包访问权限

如果根本不指定访问指示符,这意味着当前包内的其他所有类都能访问成员,但对包外的所有类来说,这些成员却是“私有”(Private)的,外界不得访问。

为获得对一个访问权限,唯一的方法就是:
(1) 使成员成为“public”(公共的)。这样所有人从任何地方都可以访问它。
(2) 变成一个“友好”成员,方法是舍弃所有访问指示符,并将其类置于相同的包内。这样一来,其他类就可以访问成员。
(3) 正如以后引入“继承”概念后大家会知道的那样,一个继承的类既可以访问一个protected成员,也可以访问一个public成员(但不可访问private成员)。只有在两个类位于相同的包内时,它才可以访问友好成员。但现在不必关心这方面的问题。
(4) 提供“访问器/变化器”方法(亦称为“获取/设置”方法),以便读取和修改值。这是OOP环境中最正规的一种方法,也是Java Beans的基础——具体情况会在第13章介绍。

下面举书上的两段程序加深对包访问权限的理解:

//: Cookie.java
// Creates a library
package c05.dessert;

public class Cookie {
  public Cookie() {
   System.out.println("Cookie constructor");
  }
  void foo() { System.out.println("foo"); }
} ///:~
 //: Dinner.java
// Uses the library
import c05.dessert.*;

public class Dinner {
  public Dinner() {
   System.out.println("Dinner constructor");
  }
  public static void main(String[] args) {
    Cookie x = new Cookie();
    //! x.foo(); // Can't access
  }

} ///:~

以上程序可以创建一个Cookie对象,因为它的构建器是public的,而且类也是public的(公共类的概念稍后还会进行更详细的讲述)。然而,foo()成员不可在Dinner.java内访问,因为foo()只有在dessert包内才是“友好”的。

4.protected–包访问权限的一种(友好的一种)

为了便于理解,举例如下:

import c05.dessert.*;

public class ChocolateChip extends Cookie {

public ChocolateChip() { System.out.println( “ChocolateChip constructor”); }

public static void main(String[] args) {

ChocolateChip x = new ChocolateChip();

//! x.foo(); // Can’t access foo } } ///:~

对于继承,值得注意的一件有趣的事情是倘若方法foo()存在于类Cookie中,那么它也会存在于从Cookie继承的所有类中。但由于foo()在外部的包里是“友好”的,所以我们不能使用它。当然,亦可将其变成public。但这样一来,由于所有人都能自由访问它,所以可能并非我们所希望的局面。若象下面这样修改类Cookie:

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

那么仍然能在包dessert里“友好”地访问foo(),但从Cookie继承的其他东西亦可自由地访问它。然而,它并非公共的(public)。

由上可见,java中的protected与C++的protected是由很大区别的,再c++中,如果再成员前加了关键词protected,那么由类生成的对象不能直接方问该成员(以a.member的形式访问,其中a为对象,member为protected),必须通过成员函数来访问,继承类也可以通过这样的方法访问,而java可以以a.member的形式访问。另外,再java中protected还提供包访问权限。

5.类访问

注意不可将类设成private(那样会使除类之外的其他东西都不能访问它),也不能设成protected。

若构造器是私有的,该如何构造对象呢,示例如下:

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

class Soup {
  private Soup() {}
  // (1) Allow creation via static method:
  public static Soup makeSoup() {
    return new Soup();
  }
  // (2) Create a static object and
  // return a reference upon request.
  // (The "Singleton" pattern):
  private static Soup ps1 = new Soup();
  public static Soup access() {
    return ps1;
  }
  public void f() {}
}

class Sandwich { // Uses Lunch
  void f() { new Lunch(); }
}

// Only one public class allowed per file:
public class Lunch {
  void test() {
    // Can't do this! Private constructor:
    //! Soup priv1 = new Soup();
    Soup priv2 = Soup.makeSoup();
    Sandwich f1 = new Sandwich();
    Soup.access().f();
  }
} ///:~

第一个选择,我们可创建一个static方法,再通过它创建一个新的Soup,然后返回指向它的一个句柄。如果想在返回之前对Soup进行一些额外的操作,或者想了解准备创建多少个Soup对象(可能是为了限制它们的个数),这种方案无疑是特别有用的。
第二个选择是采用“设计方案”(Design Pattern)技术,本书后面会对此进行详细介绍。通常方案叫作“独子”,因为它仅允许创建一个对象。类Soup的对象被创建成Soup的一个static private成员,所以有一个而且只能有一个。除非通过public方法access(),否则根本无法访问它。

Tags: java学习 | No Comments »

Comments

Spam Protection by WP-SpamFree