7.JAVA编程思想笔记隐藏实施过程

 

欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/51040237

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

      

Java 推出了“访问指示符”的概念,允许库创建者声明哪些东西是客户程序员可以使用的,哪些是不可使用的。这种访问控制的级别在“最大访问”和“最小访问”的范围之间,分别包括:public,“友好的”(无关键字),protected以及private。根据前一段的描述,大家或许已总结出作为一名库设计者,应将所有东西都尽可能保持为“private”(私有),并只展示出那些想让客户程序员使用的方法。这种思路是完全正确的,尽管它有点儿违背那些用其他语言(特别是 C)编程的人的直觉,那些人习惯于在没有任何限制的情况下访问所有东西。

组件库以及控制谁能访问那个库的组件的概念现在仍不是完整的。仍存在这样一个问题:如何将组件绑定到单独一个统一的库单元里。这是通过Java 的package(打包)关键字来实现的,而且访问指示符要受到类在相同的包还是在不同的包里的影响。

库组件如何置入包里? 这样才能理解访问指示符的完整含义。

 

1      包:库单元

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

import java.util.*;

它的作用是导入完整的实用工具(Utility)库,该库属于标准Java 开发工具包的一部分。

由于Vector 位于java.util 里,所以现在要么指定完整名称“java.util.Vector”(可省略 import 语句),要么简单地指定一个“Vector”(因为import是默认的)。 若想导入单独一个类,可在import语句里指定那个类的名字: importjava.util.Vector; 现在,可以自由地使用Vector。然而,java.util 中的其他任何类仍是不可使用的。

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

类(由其他人编写)的机器上,这时会出现什么情况呢?对于因特网中的 Java 应用,这种情况会在用户毫不知晓的时候发生,因为类会在运行一个Java 程序的时候自动下载。

正是由于存在名字潜在的冲突,所以特别有必要对 Java 中的命名空间进行完整的控制,而且需要创建一个完全独一无二的名字,无论因特网存在什么样的限制。

若计划创建一个“对因特网友好”或者说“适合在因特网使用”的程序,必须考虑如何防止类名的重复。

为Java 创建一个源码文件的时候,它通常叫作一个“编辑单元”(有时也叫作“翻译单元”)。每个编译单元都必须有一个以.java结尾的名字。而且在编译单元的内部,可以有一个公共(public)类,它必须拥有与文件相同的名字(包括大小写形式,但排除.java文件扩展名)。如果不这样做,编译器就会报告出错。

每个编译单元内都只能有一个public 类(同样地,否则编译器会报告出错)。那个编译单元剩下的类(如果有的话)可在那个包外面的世界面前隐藏起来,因为它们并非“公共”的(非public),而且它们由用于主public类的“支撑”类组成。

编译一个.java 文件时,我们会获得一个名字完全相同的输出文件;但对于.java 文件中的每个类,它们都有一个.class 扩展名。因此,我们最终从少量的.java 文件里有可能获得数量众多的.class 文件。

如以前用一种汇编语言写过程序,那么可能已习惯编译器先分割出一种过渡形式(通常是一个.obj 文件),再用一个链接器将其与其他东西封装到一起(生成一个可执行文件),或者与一个库封装到一起(生成一个库)。但那并不是Java 的工作方式。一个有效的程序就是一系列.class 文件,它们可以封装和压缩到一个 JAR文件里

“库”也由一系列类文件构成。每个文件都有一个 public类(并没强迫使用一个 public 类,但这种情况最很典型的),所以每个文件都有一个组件。如果想将所有这些组件(它们在各自独立的.java 和.class文件里)都归纳到一起,那么package 关键字就可以发挥作用)。

若在一个文件的开头使用下述代码:

package mypackage;

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

作为一名库设计者,一定要记住package和import关键字允许我们做的事情就是分割单个全局命名空间,保证我们不会遇到名字的冲突——无论有多少人使用因特网,也无论多少人用Java 编写自己的类。

 

1.1      创建独一无二的包名

 

由于一个包永远不会真的“封装”到单独一个文件里面,它可由多个.class 文件构成,所以局面可能稍微有些混乱。为避免这个问题,最合理的一种做法就是将某个特定包使用的所有.class文件都置入单个目录里。这正是 Java 所采取的方法。

同时也解决了另两个问题:创建独一无二的包名以及找出那些可能深藏于目录结构某处的类。

但根据约定,编译器强迫package名的第一部分是类创建者的因特网域名。由于因特网域名肯定是独一无二的(由InterNIC保证,它控制着域名的分配),所以假如按这一约定行事,package 的名称就肯定不会重复,所以永远不会遇到名称冲突的问题。换句话说,除非将自己的域名转让给其他人,而且对方也按照相同的路径名编写Java 代码,否则名字的冲突是永远不会出现的。当然,如果你没有自己的域名,那么必须创造一个非常生僻的包名(例如自己的英文姓名),以便尽最大可能创建一个独一无二的包名。如决定发行自己的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 解释器驻留的地方有关。

自Java 1.2 以来,整个包名都是小写的. CLASSPATH 里能包含大量备用的搜索路径.

 

1.1.1       自动编译

为导入的类首次创建一个对象时(或者访问一个类的static 成员时),编译器会在适当的目录里寻找同名的.class 文件(所以如果创建类 X的一个对象,就应该是 X.class)。若只发现X.class,它就是必须使用的那一个类。然而,如果它在相同的目录中还发现了一个X.java,编译器就会比较两个文件的日期标记。如果X.java 比X.class 新,就会自动编译X.java,生成一个最新的X.class。

对于一个特定的类,或在与它同名的.java 文件中没有找到它,就会对那个类采取上述的处理。

1.1.2       冲突

若通过*导入了两个库,而且它们包括相同的名字.例如,假定一个程序使用了下述

导入语句:

import com.bruceeckel.util.*;

import java.util.*;

由于java.util.*也包含了一个Vector 类,所以这会造成潜在的冲突。然而,只要冲突并不真的发生,那么就不会产生任何问题——这当然是最理想的情况,因为否则的话,就需要进行大量编程工作,防范那些可能可能永远也不会发生的冲突。

如现在试着生成一个Vector。如下所示:

Vector v = new Vector();

它引用的到底是哪个Vector类呢?编译器对这个问题没有答案,读者也不可能知道。所以编译器会报告一个错误,强迫我们进行明确的说明。例如,假设我想使用标准的 Java Vector,那么必须象下面这样编程:

java.util.Vector v = newjava.util.Vector();

由于它(与CLASSPATH 一起)完整指定了那个Vector的位置,所以不再需要import java.util.*语句,除非还想使用来自java.util 的其他东西。

蛤蟆对JAVA的这块设计不得不点个赞,相当NICE.

 

1.2      自定义工具库

创建自己的工具库,以便减少或者完全消除重复的代码。

可为System.out.println()创建一个别名,减少重复键入的代码量。

package com.toad;

 

publicclass P {

    publicstaticvoid rint(Object obj) {

        System.out.print(obj);

    }

 

    publicstaticvoid rint(String s) {

        System.out.print(s);

    }

 

    publicstaticvoid rint(char[]s) {

        System.out.print(s);

    }

 

    publicstaticvoid rint(charc) {

        System.out.print(c);

    }

 

    publicstaticvoid rint(inti) {

        System.out.print(i);

    }

 

    publicstaticvoid rint(longl) {

        System.out.print(l);

    }

 

    publicstaticvoid rint(floatf) {

        System.out.print(f);

    }

 

    publicstaticvoid rint(doubled) {

        System.out.print(d);

    }

 

    publicstaticvoid rint(booleanb) {

        System.out.print(b);

    }

 

    publicstaticvoid rintln() {

        System.out.println();

    }

 

    publicstaticvoid rintln(Object obj) {

        System.out.println(obj);

    }

 

    publicstaticvoid rintln(String s) {

        System.out.println(s);

    }

 

    publicstaticvoid rintln(char[]s) {

        System.out.println(s);

    }

 

    publicstaticvoid rintln(charc) {

        System.out.println(c);

    }

 

    publicstaticvoid rintln(inti) {

        System.out.println(i);

    }

 

    publicstaticvoid rintln(longl) {

        System.out.println(l);

    }

 

    publicstaticvoid rintln(floatf) {

        System.out.println(f);

    }

 

    publicstaticvoid rintln(doubled) {

        System.out.println(d);

    }

 

    publicstaticvoid rintln(booleanb) {

        System.out.println(b);

    }

} // /:~

不同的数据类型现在都可以在一个新行输出(P.rintln()),或者不在一个新行输出(P.rint())。

这个文件所在的目录必须从某个CLASSPATH位置开始。编译完毕后,利用一个import 语句,即可在自己系统的任何地方使用P.class文件。

测试这个文件

import com.toad.*;

 

publicclass testtool {

 publicstaticvoid main(String[] args) {

   P.rintln("Available from now on!");

 }

} ///:~

目录如下图:

输出如下:

Availablefrom now on!

后续只要做出了一个有用的新工具,就可将其加入toad 目录即可。

不过请务必保证对于类路径的每个地方,每个名字都仅存在一个类。

 

1.3      利用导入改变行为

Java 已取消的一种特性是C 的“条件编译”,它允许我们改变参数,获得不同的行为,同时不改变其他任何代码。Java 之所以抛弃了这一特性,可能是由于该特性经常在 C里用于解决跨平台问题:代码的不同部分根据具体的平台进行编译,否则不能在特定的平台上运行。由于 Java 的设计思想是成为一种自动跨平台的语言,所以这种特性是没有必要的。

然而,条件编译还有另一些非常有价值的用途。一种很常见的用途就是调试代码。调试特性可在开发过程中使用,但在发行的产品中却无此功能。Alen Holub(www.holub.com)提出了利用包(package)来模仿条件编译的概念。根据这一概念,它创建了C“断定机制”一个非常有用的Java版本。之所以叫作“断定机制”,是由于我们可以说“它应该为真”或者“它应该为假”。如果语句不同意你的断定,就可以发现相关的情况。这种工具在调试过程中是特别有用的。

创建类如下:

package com.toad;

publicclass Assert {

    privatestaticvoid perr(String msg) {

        System.err.println(msg);

    }

 

    publicfinalstaticvoid is_true(booleanexp) {

        if (!exp)

            perr("Assertionfailed");

    }

 

    publicfinalstaticvoid is_false(booleanexp) {

        if (exp)

            perr("Assertionfailed");

    }

 

    publicfinalstaticvoid is_true(booleanexp, Stringmsg) {

        if (!exp)

            perr("Assertionfailed: " +msg);

    }

 

    publicfinalstaticvoid is_false(booleanexp, Stringmsg) {

        if (exp)

            perr("Assertionfailed: " +msg);

    }

} // /:~

 

在不同路径下创建Assert.java如下:

package com.debug;

 

publicclass Assert {

 publicfinalstaticvoid is_true(booleanexp){}

 publicfinalstaticvoid is_false(booleanexp){}

 publicfinalstaticvoid 

 is_true(booleanexp, Stringmsg) {}

 publicfinalstaticvoid 

 is_false(booleanexp, Stringmsg) {}

} ///:~

 

使用示例如下:

importcom.debug.*;

//import com.toad.*;

publicclass TestAssert {

  public static void main(String[] args) {

    Assert.is_true((2 + 2) == 5);

    Assert.is_false((1 + 1) == 2);

    Assert.is_true((2 + 2) == 5, "2 + 2 ==5");

    Assert.is_false((1 + 1) == 2, "1 +1 !=2");

  }

}///:~

    通过改变导入的package,我们可将自己的代码从调试版本变成最终的发行版本。这种技术可应用于任何种类的条件代码。

1.4      包的停用

每次创建一个包后,都在为包取名时间接地指定了一个目录结构。这个包必须存在(驻留)于由它的名字规定的目录内。而且这个目录必须能从CLASSPATH 开始搜索并发现。最开始的时候,package关键字的运用可能会令人迷惑,因为除非坚持遵守根据目录路径指定包名的规则,否则就会在运行期获得大量莫名其妙的消息,指出找不到一个特定的类——即使那个类明明就在相同的目录中。请试着将package 语句作为注释标记出去。如果这样做行得通,就可知道问题到底出在哪儿。

 

2      Java 访问指示符

针对类内每个成员的每个定义,Java 访问指示符 poublic,protected 以及private都置于它们的最前面——无论它们是一个数据成员,还是一个方法。每个访问指示符都只控制着对那个特定定义的访问。这与C++存在着显著不同。在C++中,访问指示符控制着它后面的所有定义,直到又一个访问指示符加入为止。

 

2.1      “友好的”

如果根本不指定访问指示符,默认的访问没有关键字,但它通常称为“友好”(Friendly)访问。这意味着当前包内的其他所有类都能访问“友好的”成员,

但对包外的所有类来说,这些成员却是“私有”(Private)的,外界不得访问。由于一个编译单元(一个文件)只能从属于单个包,所以单个编译单元内的所有类相互间都是自动“友好”的。因此,我们也说友好元素拥有“包访问”权限。

友好访问允许我们将相关的类都组合到一个包里,使它们相互间方便地进行沟通。将类组合到一个包内以后,便“拥有”了那个包内的代码。只有我们已经拥有的代码才能友好地访问自己拥有的其他代码。我们可认为友好访问使类在一个包内的组合显得有意义,或者说前者是后者的原因。在许多语言中,我们在文件内组织定义的方式往往显得有些牵强。但在Java 中,却强制用一种颇有意义的形式进行组织。除此以外,我们有时可能想排除一些类,不想让它们访问当前包内定义的类。

对于任何关系,一个非常重要的问题是“谁能访问我们的‘私有’或private代码”。类控制着哪些代码能够访问自己的成员。没有任何秘诀可以“闯入”。另一个包内推荐可以声明一个新类,然后说:“嗨,我是Bob的朋友!”,并指望看到Bob的“protected”(受到保护的)、友好的以及“private”(私有)的成员。为获得对一个访问权限,唯一的方法就是:

(1) 使成员成为“public”(公共的)。这样所有人从任何地方都可以访问它。

(2) 变成一个“友好”成员,方法是舍弃所有访问指示符,并将其类置于相同的包内。这样一来,其他类就可以访问成员。

(3) 正如以后引入“继承”概念后大家会知道的那样,一个继承的类既可以访问一个 protected 成员,也可以访问一个 public成员(但不可访问 private成员)。只有在两个类位于相同的包内时,它才可以访问友好成员。但现在不必关心这方面的问题。

(4) 提供“访问器/变化器”方法(亦称为“获取/设置”方法),以便读取和修改值。

 

2.2      public:接口访问

使用public关键字时,它意味着紧随在public 后面的成员声明适用于所有人,特别是适用于使用库的客户程序员。

创建类如下:

publicclass Dinner {

 public Dinner() {

  System.out.println("Dinner constructor");

 }

 publicstaticvoid main(String[] args) {

   Cookie x =new Cookie();

   //!x.foo(); // Can't access

 }

} ///:~

创建类如下:

publicclass Cookie {

 public Cookie() { 

  System.out.println("Cookieconstructor"); 

 }

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

} ///:~

创建一个Cookie对象,因为它的构建器是public的,而且类也是public的。然而,foo()成员不可在 Dinner.java 内访问,因为foo()只有在包内才是“友好”的。

2.2.1       默认包

可能会惊讶地发现下面这些代码得以顺利编译。

 

classCake {

  public static void main(String[] args) {

    Pie x = new Pie();

    x.f();

  }

}///:~

 

在位于相同目录的第二个文件里:

 

//:Pie.java

//The other class

 

classPie {

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

}///:~

 

Cake 能创建一个 Pie对象,并能调用它的f()方法!通常的想法会认为Pie和f()是“友好的”,所以不适用于Cake。它们确实是友好的——这部分结论非常正确。但它们之所以仍能在Cake.java 中使用,是由于它们位于相同的目录中,而且没有明确的包名。Java 把象这样

的文件看作那个目录“默认包”的一部分,所以它们对于目录内的其他文件来说是“友好”的。

 

2.3      private 

private关键字意味着除非那个特定的类,而且从那个类的方法里,否则没有人能访问那个成员。同一个包内的其他成员不能访问 private成员,这使其显得似乎将类与我们自己都隔离起来。另一方面,也不能由几个合作的人创建一个包。所以private 允许我们自由地改变那个成员,同时毋需关心它是否会影响同一个包内的另一个类。默认的“友好”包访问通常已经是一种适当的隐藏方法;请记住,对于包的用户来说,是不能访问一个“友好”成员的。这种效果往往能令人满意,因为默认访问是我们通常采用的方法。对于希望变成public(公共)的成员,我们通常明确地指出,令其可由客户程序员自由调用。而且作为一个结果,最开始的时候通常会认为自己不必频繁使用private关键字,因为完全可以在不用它的前提下发布自己的代码(这与C++是个鲜明的对比)。private有非常重要的用途,特别是在涉及多线程处理的时候。

下面是应用了private 的一个例子:

package com.main;

class Sundae {

 private Sundae() {System.out.println("privateSundate");}

 static Sundae makeASundae() { 

   returnnew Sundae(); 

 }

}

 

publicclass IceCream {

 publicstaticvoid main(String[] args) {

   //!Sundaex = new Sundae();

   Sundae x =Sundae.makeASundae();

 }

} ///:~

这个例子向我们证明了使用private的方便:有时可能想控制对象的创建方式,并防止有人直接访问一个特定的构建器(或者所有构建器)。在上面的例子中,我们不可通过它的构建器创建一个Sundae 对象;相反,必须调用makeASundae()方法来实现。此时还会产生另一个影响:由于默认构建器是唯一获得定义的,而且它的属性是 private,所以可防止对这个类的继承。

    若确定一个类只有一个“助手”方法,那么对于任何方法来说,都可以把它们设为private,从而保证自己不会误在包内其他地方使用它,防止自己更改或删除方法。将一个方法的属性设为private后,可保证自己一直保持这一选项(然而,若一个句柄被设为 private,并不表明其他对象不能拥有指向同一个对象的public句柄。

 

2.4      protected

protected 关键字为我们引入了一种名为“继承”的概念,它以现有的类为基础,并在其中加入新的成员,同时不会对现有的类产生影响——我们将这种现有的类称为“基础类”或者“基本类”(Base Class)。亦可改变那个类现有成员的行为。对于从一个现有类的继承,我们说自己的新类“扩展”(extends)了那个现有的类。如下所示:

class Foo extends Bar {

类定义剩余的部分看起来是完全相同的。

若新建一个包,并从另一个包内的某个类里继承,则唯一能够访问的成员就是原来那个包的public成员。当然,如果在相同的包里进行继承,那么继承获得的包能够访问所有“友好”的成员。有些时候,基础类的创建者喜欢提供一个特殊的成员,并允许访问衍生类。这正是protected的工作。

扩展之前的cookie类如下:

package com.main;

publicclass ChocolateChipextends Cookie {

 public ChocolateChip() {

  System.out.println(

    "ChocolateChipconstructor");

 }

 publicstaticvoid main(String[] args) {

   ChocolateChip x =new ChocolateChip();

   //!x.foo(); // Can't accessfoo

 }

} ///:~

对于继承,值得注意的一件有趣的事情是倘若方法 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)。

 

2.5      接口与实现

我们通常认为访问控制是“隐藏实施细节”的一种方式。将数据和方法封装到类内后,可生成一种数据类型,它具有自己的特征与行为。但由于两方面重要的原因,访问为那个数据类型加上了自己的边界。

第一个原因是规定客户程序员哪些能够使用,哪些不能。我们可在结构里构建自己的内部机制,不用担心客户程序员将其当作接口的一部分,从而自由地使用或者“滥用”。

这个原因直接导致了第二个原因:我们需要将接口同实施细节分离开。若结构在一系列程序中使用,但用户除了将消息发给public接口之外,不能做其他任何事情,我们就可以改变不属于 public 的所有东西(如“友好的”、protected 以及private),同时不要求用户对他们的代码作任何修改。我们现在是在一个面向对象的编程环境中,其中的一个类(class)实际是指“一类对象”。从属于这个类的所有对象都共享这些特征与行为。“类”是对属于这一类的所有对象的外观及行为进行的一种描述。

在一些早期 OOP语言中,如 Simula-67,关键字class 的作用是描述一种新的数据类型。同样的关键字在大多数面向对象的编程语言里都得到了应用。

Java中,类是最基本的OOP概念。为清楚起见,可考虑用特殊的样式创建一个类:将 public成员置于最开头,后面跟随protected、友好以及private成员。这样做的好处是类的使用者可从上向下依次阅读,并首先看到对自己来说最重要的内容(即public成员,因为它们可从文件的外部访问),并在遇到非公共成员后停止阅读,后者已经属于内部实施细节的一部分了。然而,利用由javadoc 提供支持的注释文档,代码的可读性问题已在很大程度上得到了解决。

 

由于接口和实施细节仍然混合在一起,所以只是部分容易阅读。也就是说,仍然能够看到源码——实施的细节,因为它们需要保存在类里面。向一个类的消费者显示出接口实际是“类浏览器”的工作。这种工具能查找所有可用的类,总结出可对它们采取的全部操作(比如可以使用哪些成员等),并用一种清爽悦目的形式显示出来。

 

 

2.6      类访问

在Java 中,可用访问指示符判断出一个库内的哪些类可由那个库的用户使用。若想一个类能由客户程序员调用,可在类主体的起始花括号前面某处放置一个 public关键字。它控制着客户程序员是否能够创建属于这个类的一个对象。

为控制一个类的访问,指示符必须在关键字class 之前出现。所以我们能够使用:

public class Widget {

也就是说,假若我们的库名是mylib,那么所有客户程序员都能访问Widget——通过下述语句:

import mylib.Widget;

或者

import mylib.*;

然而,我们同时还要注意到一些额外的限制:

(1) 每个编译单元(文件)都只能有一个public。每个编译单元有一个公共接口的概念是由那个公共类表达出来的。根据自己的需要,它可拥有任意多个提供支撑的“友好”类。但若在一个编译单元里使用了多个public类,编译器就会向我们提示一条出错消息。

(2) public类的名字必须与包含了编译单元的那个文件的名字完全相符,甚至包括它的大小写形式。所以对于Widget 来说,文件的名字必须是Widget.java,而不应是 widget.java或者WIDGET.java。同样地,如果出现不符,就会报告一个编译期错误。

(3) 可能(但并常见)有一个编译单元根本没有任何公共类。此时,可按自己的意愿任意指定文件名。

   将public 关键字从类中剔除即可,这样便把类变成了“友好的”(类仅能在包内使用)。

注意不可将类设成private(那样会使除类之外的其他东西都不能访问它),也不能设成protected。因此,我们现在对于类的访问只有两个选择:“友好的”或者public。若不愿其他任何人访问那个类,可将所有构建器设为private。这样一来,在类的一个static 成员内部,除自己之外的其他所有人都无法创建属于那个类的一个对象。

示例如下:

package com.main;

 

class Soup {

    private Soup() {

        System.out.println("privatesoup");

    }

 

    // (1) Allow creationvia static method:

    publicstatic Soup makeSoup() {

        returnnew Soup();

    }

 

    // (2) Create astatic object and

    // return a referenceupon request.

    // (The"Singleton" pattern):

    privatestatic Soupps1 =new Soup();

 

    publicstatic Soup access() {

        returnps1;

    }

 

    publicvoid f() {

        System.out.println("soup.f()");

    }

}

 

class Sandwich {// Uses Lunch

    void f() {

        new Lunch();

    }

}

 

// Only one public class allowed perfile:

publicclass Lunch {

    void test() {

        // Can't do this!Private constructor:

        // ! Soup priv1 = newSoup();

        Souppriv2 = Soup.makeSoup();

        Sandwichf1 =new Sandwich();

        Soup.access().f();

    }

    Lunch(){

        System.out.println("Lunch");

    }

   

} // /:~

Soup 类向我们展示出如何通过将所有构建器都设为 private,从而防止直接创建一个类。假若不明确地至少创建一个构建器,就会自动创建默认构建器(没有自变量)。若自己编写默认构建器,它就不会自动创建。把它变成private后,就没人能为那个类创建一个对象。但别人怎样使用这个类呢?上面的例子为我们揭示出了两个选择。

第一个选择,我们可创建一个static 方法,再通过它创建一个新的Soup,然后返回指向它的一个句柄。如果想在返回之前对Soup 进行一些额外的操作,或者想了解准备创建多少个 Soup 对象(可能是为了限制它们的个数),这种方案无疑是特别有用的。

第二个选择是采用“设计方案”(Design Pattern)技术。通常方案叫作“独子”,因为它仅允许创建一个对象。类Soup 的对象被创建成Soup 的一个 static private 成员,所以有一个而且只能有一个。除非通过public 方法access(),否则根本无法访问它。如果不针对类的访问设置一个访问指示符,那么它会自动默认为“友好的”。这意味着那个类的对象可由包内的其他类创建,但不能由包外创建。对于相同目录内的所有文件,如果没有明确地进行package声明,那么它们都默认为那个目录的默认包的一部分。然而,假若那个类一个static成员的属性是public,那么客户程序员仍然能够访问那个static 成员——即使它们不能创建属于那个类的一个对象。

添加测试文件test.java如下:

package com.main;

importcom.debug.*;

// import com.toad.*;

publicclass test {

 publicstaticvoid main(String[] args) {

   Lunch l=new Lunch();

   l.test();

 }

} ///:~

最后输出如下:

Lunch

privatesoup

privatesoup

soup.f()

 

 

3      总结

创建一个库时,相当于建立了同那个库的用户(即“客户程序员”)的一种关系——那些用户属于另外的程序员,可能用我们的库自行构建一个应用程序,或者用我们的库构建一个更大的库。

如果不制订规则,客户程序员就可以随心所欲地操作一个类的所有成员,无论我们本来愿不愿意其中的一些成员被直接操作。所有东西都在别人面前都暴露无遗。

由于C仅有一个“命名空间”,所以名字会开始互相抵触,从而造成额外的管理开销。而在Java 中,package关键字、包命名方案以及import关键字为我们提供对名字的完全控制,所以命名冲突的问题可以很轻易地得到避免。

有两方面的原因要求我们控制对成员的访问。第一个是防止用户接触那些他们不应碰的工具。对于数据类型的内部机制,那些工具是必需的。但它们并不属于用户接口的一部分,用户不必用它来解决自己的特定问题。所以将方法和字段变成“私有”(private)后,可极大方便用户。因为他们能轻易看出哪些对于自己来说是最重要的,以及哪些是自己需要忽略的。这样便简化了用户对一个类的理解。

进行访问控制的第二个、也是最重要的一个原因是:允许库设计者改变类的内部工作机制,同时不必担心它会对客户程序员产生什么影响。最开始的时候,可用一种方法构建一个类,后来发现需要重新构建代码,以便达到更快的速度。如接口和实施细节早已进行了明确的分隔与保护,就可以轻松地达到自己的目的,不要求用户改写他们的代码。

利用Java 中的访问指示符,可有效控制类的创建者。那个类的用户可确切知道哪些是自己能够使用的,哪些则是可以忽略的。但更重要的一点是,它可确保没有任何用户能依赖一个类的基础实施机制的任何部分。作为一个类的创建者,我们可自由修改基础的实施细节,这一改变不会对客户程序员产生任何影响,因为他们不能访问类的那一部分。

有能力改变基础的实施细节后,除了能在以后改进自己的设置之外,也同时拥有了“犯错误”的自由。无论当初计划与设计时有多么仔细,仍然有可能出现一些失误。由于知道自己能相当安全地犯下这种错误,所以可以放心大胆地进行更多、更自由的试验。这对自己编程水平的提高是很有帮助的,使整个项目最终能更快、更好地完成。

一个类的公共接口是所有用户都能看见的,所以在进行分析与设计的时候,这是应尽量保证其准确性的最重要的一个部分。但也不必过于紧张,少许的误差仍然是允许的。若最初设计的接口存在少许问题,可考虑添加更多的方法,只要保证不删除客户程序员已在他们的代码里使用的东西。

 

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值