Java学习笔记——访问权限控制

时间:2022-02-20 15:19:38

本文为Java编程思想第四版的学习笔记,在此感谢作者Bruce Eckel给我们带来这样一本经典著作,也感谢该书的翻译及出版人员。本文为原创笔记,转载请注明出处,谢谢。


访问控制(或隐藏具体实现)与“最初的实现并不恰当”有关。如果你把一个代码段放到了某个位置,等过一会儿回头再看时,有可能会发现有更好的方式去实现相同的功能。这正是重构的原动力之一,重构即重写代码,以使它更可读、更易理解,并因此而更具可维护性。但是,在这种修改和完善代码的愿望之下,也存在着巨大的压力。通常总是会有一些消费者(客户端程序员)需要你的代码在某些方面保持不变。因此你想改变代码,因而他们却想让代码保持不变。由此而产生了在面向对象设计中需要考虑的一个基本问题:“如何把变动的事物与保持不变的事物区分开来”。这对类库而言尤为重要,类库的开发者必须有权限进行修改和改进,并确保客户代码不会因为这些改动而受到影响。

为了解决这一个问题,Java提供了访问权限修饰词,以供类库开发人员向客户端程序员指明那些是可用的,哪些是不可用的。访问权限控制的等级,从最大权限到最小权限一次为:public、protected、包访问权限(没有关键词)和private。不过,构件类库的概念以及对于谁有权取用该类库构件的控制问题都还不是完善的。其中仍旧存在着如何将构件捆绑到一个内聚的类库但愿中的问题。对于这一点,Java用关键字package加以控制,而访问权限修饰词会因类是存在于一个相同的包,还是存在于一个单独的包而受到影响。因此,首先要学习如何将类库构件至于包中,然后就会理解访问权限修饰词的全部含义。


1.包:库单元

包内包含有一组类,它们在单一的名字空间之下被组织在了一起。例如,在Java的标准发布中有一个工具库,它被组织在java.util名字空间之下,java.util中有一个叫做ArrayList的类,使用ArrayList的一种方式是用其全名java.util.ArrayList来指定。这立刻就使程序变得很冗长了,因此你可能想转而使用import关键字。我们之所以要导入,就是提供一个管理名字空间的机制,由于名字之间的潜在冲突,在Java中对名称空间进行完全控制并为每个类创建唯一的标识符组合就成为了非常重要的事情。如果你正在准备编写对在同一台机器上共存的其他Java程序有好的类库或程序的话,就需要考虑如何防止类名称之间的冲突问题。

当编写一个Java源码文件时,此文件通常被称为编译单元(有时也被称为转义单元)。每个编译单元都必须有一个后缀名.java的文件,而在编译单元内则可以有一个public类,该类的名称必须与文件的名称相同(包括大小写,但不包括文件的后缀名.java)。每个编译单元只能有一个public类,否则编译器就不会接受。如果在该编译单元之中还有额外的类的话,那么在包之外的世界是无法看见这些类的。就是因为他们不是public类,而且他们主要用来为public类提供支持。


1.1 代码组织

当编译一个.java文件时,在java文件中的每个类都会有一个输出文件,而该输出文件的名称与java文件中每个类的名称相同,只是多了一个后缀名.class。因此,在少量的.java文件之后,会得到大量的.class文件。

Java可运行程序是一组可以打包并压缩为一个Java文档文件(JAR,使用Java的jar文档生成器)的.class文件。Java解释器负责这些文件的查找、装在和解释。

类库实际上是一组类文件。其中每个文件都有一个public类,以及任意数量的非public类。因此每个文件都有一个构件,如果希望这些构件(每一个都有他们自己的独立的.java和.class文件)从属于一个群组,就可以使用package关键字。如果使用package语句,它必须是文件中除注释以外的第一句程序代码。在文件起始处写:package access;就表示你在声明该编译单元是名为access的类库的一部分。或者换种说法,你正在声明该编译单元中的public类名是位于access名称的保护之下。任何想要使用该名称的人都必须使用前面给出的选择,指定全名或者与access结合使用关键字import(请注意,Java包的命名规则全部使用小写字母,包括中间的字也是如此。身为一名类库设计院,很有必要牢记:package和import关键字允许你做的,是将单一的全局名字空间分割开,使得无论多少人使用Internet以及Java开始编写类,都不会出现名称冲突的问题。


1.2创建独一无二的包名

读者也许会发现,既然一个包从为真正将打包的东西包装成单一的文件,并且一个包可以由许多.class文件构成,那么情况就有点复杂了。为了避免这种情况的发生,一种合乎逻辑的做法就是将特定包的所有.class文件都置于一个目录下。也就是说,利用操作系统的层次化文件结构来解决这一问题,读者还会在我们介绍jar工具的时候看到另一种方式。

将左右的文件收入一个子目录还可以解决另外两个问题:怎样创建独一无二的名称以及怎样查找有可能隐藏域目录结构中的某处的类。这些任务是通过将.class文件所在的路径位置编码成package的名称来实现的。按照惯例,package名称的第一部分是类的创建者的范顺序的Inernet域名。如果你遵照惯例,Internet域名应该是独一无二的,因此你的package名称也将是独一无二的。此技巧的第二部分是把package名称分解为你机器上的一个目录。当Java程序运行并且需要加载.class文件的时候,它就可以确定.class文件在目录上所处的位置。

Java解释器的运行过程如下:首先,找出环境变量CLASSPASS(可以通过操作系统来设置,有时也可通过安装程序用来在你的机器上安装Java或基于Java的工具来设置)CLASSPATH包含一个或多个目录,用作查找.class文件的根目录。从根目录开始,解释器获取包的名称并将每个句点替换成反斜杠,以从CLASSPATH根总产生一个路径名称(于是,package foo.bar.baz就变成为foo\bar\baz或foo/bar/baz或其他,这一切取决于操作系统)。得到的路径会与CLASSPATH中的各个不同的项相连接,解释器就在这些目录中查找与你所创建的类名称相同的.class文件(解释器还会去查找某些设计Java解释器所在未知的标准目录)。


如果将两个含有相同名称的类库以“*”形式同时倒入,将会产生冲突情况。但是只要你不写那些导致冲突的代码,就不会有什么问题。如果你需要调用这个相同名称的类,就必须用全名指定为那个库中的类,否则编译器会报错。


1.3 定制工具库

具备了这些知识后,现在就可以创建自己的工具库来减少或消除重复的代码程序了。——本菜鸟还没有自己的工具库呢Java学习笔记——访问权限控制


1.4 用import改变行为

Java没有C的条件编译功能,该功能可以使你不必改变任何程序代码,就能够切换开关并产生不同的行为。Java去掉此功能的原因可能是因为C在绝大多数情况下是用词功能来解决跨平台的问题,即程序代码的同部分是根据不同的平台来变异的。由于Java自身可以自动跨越不同的平台,因此这个功能对Java而言是没有必要的。

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


1.5 对使用包的忠告

务必记住,无论何时创建包,都已经在给定包的名称的时候隐含地指定了目录结构。这个包必须位于其名称所指定的目录之中,而该目录必须是在意CLASSPATH开始的目录中可以查询到的。


2.Java访问权限修饰词

public、protected和private这几个Java访问权限修饰词在使用时,是置于类中每个成员的定义之前的——无论是一个域还是一个方法。每个访问权限修饰词仅控制它所修饰的特定定义的访问权。如果不提供任何访问权限修饰词,则意味着它是“包访问权限”.因此无论如何,所有事物都具有某种形式的访问权限。


 2.1 包访问权限

默认访问权限没有任何关键字,但通常是指包访问权限。这意味着,当前的包中的所有其他类对那个成员都有访问权限,但对于这个包之外的所有类,这个成员确实private。由于一个编译单元(一个文件),只能隶属于一个包,所以经由包访问权限,出于同一个编译单元中的所有类彼此之前都是自动可访问的。

包访问权限允许将包内所有有关的类组合起来,以使他们彼此之间可以轻松地相互作用。应该说,包访问权限为把类群聚在一个包中的做法提供了意义和理由。类控制着那些代码有权访问自己的成员。


2.2 public:接口访问权限

使用关键字public,就意味着public之后紧跟着的成员声明自己对每个人都是可用的,尤其是使用类库的客户端程序员更是如此。

令人吃惊的是,当两个类都没有为自己指定包名时,这看起来破坏了规则,但他们仍可以编译。最初或许认为这两个文件毫不相关,但是他们的确享有饱饭温泉县,但这只是部分正确的。这两个类可以相互访问的原因是因为它们同处于相同的目录并且没有给自己设定任何包名称。Java将这样的文件自动看做是隶属于该目录的默认包之中,于是它们为该目录中所有其他文件都提供了包访问权限。


2.3 private:你无法访问

关键字private的意思是,除了包含该成员的类之外,其他任何类都无法访问这个成员。由于出于同一个包内的其他类是不可以访问private成员的,因此这等于说是自己隔离了自己。从另一方面说,让许多人共同合作来创建一个包也是不大可能的,为此private就允许你随意改变该成员,而不必考虑这样做是否会影响到包内其他的类。

默认的包访问权限通常已经提供了充足的隐藏措施。请记住,使用类的客户端程序员是无法访问包访问权限的。任何可以肯定只是该类的一个“助手”方法的方法,都可以把它指定为private,以确保不会再包内的其他地方误用到它,于是也就防止了你会去改变或删除这个方法。将方法指定为private确保了你拥有这种选择权。这对于类中的private域同样适用。除非必须公开底层实现细目(这种境况很少见),否则就应该将所有的域指定为private。然而,不能因为在类中某个对象的引用是private,就认为其他的对象无法拥有该对象的public引用。


2.4 protected:继承访问权限

关键字protected处理的是继承的概念,如果创建了一个新包,并自另一个包中继承类,那么唯一可以访问的成员就是源包的public成员。(当然,如果在同一个包内执行继承工作,就可以操纵所有的拥有包访问权限的成员)。有时,基类的创建者会希望有某个特定成员,把它的访问权限赋予派生类而不是所有类。这就需要protected来完成这一工作。proected也提供包访问权限,也就是说,相同包内的其他类可以访问protected元素。


3.接口和实现

访问权限的控制常被称为是具体实现的隐藏。把数据和方法包装进类中,以及具体实现的隐藏,常共同呗乘坐是封装。其结果是一个同时带有特征和行为的数据类型。

出于两个很重要的原因,访问权限控制将权限的边界划在了数据类型的内部。第一个原因是要设定客户端程序员可以使用和不可以使用的接线。可以在结构中简历自己的内部机制,而不必担心客户端程序员偶然地将内部机制当做是他们可以使用的接口的一部分。这个原因直接引出了第二个原因,即将接口和具体实现进行分离。如果结构是用于一组程序之中,而客户端程序员除了可以向接口发送信息之外很么也不可以做的话,那么就可以随意更改所有不是public的东西(例如有包访问权限、protected和private的成员),而不会破坏客户端代码。


4.类的访问权限

在Java中,访问权限修饰词也可以用于确定库中的哪些类对于该哭的使用者是可用的。如果希望某个类可以为某个客户端程序员所用,就可以通过把关键字public作用域整个类的定义来达到目的。这样做甚至可以控制客户端程序员是否能创建一个该类的对象。为了控制某个类的访问权限,修饰词必须出现于关键字class之前。然而,这里还有一些额外的限制:

1)每个编译单元(文件)都只能有一个public类。这表示每个编译单元都有单一的公共接口,用public类来表现。该接口可以按要求包含众多的支持包访问权限的类。如果在某个编译单元内有一个以上的public类,编译器就会给出出错信息。

2)public类的名称必须完全与俺有编译单元的文件名相匹配,包括大小写。所以对于Widget而言,文件名称必须是Widget.java,如果不匹配,同样将得到编译时错误。

3)虽然不是很常用,但编译单元内完全不带public类也是可能的。在这种情况下,可以随意对文件取名。(尽管随意命名会使得人们在阅读和维护代码时产生混淆)。



在创建一个包访问权限的类时,仍旧是在将该类的域声明为private时才有意义——尽可能低总是将域指定为私有的,但是通常来说,将与类(包访问权限)相同的访问权限赋予方法也是很合理的。既然有一个包访问权限的类通常只能用于包内,那么如果你有强制要求,在此种情况下,编译器会告诉你,你只需要将这样的类的方法设定为public就可以了。

请注意,类既不可以是private的(这样会使得除该类之外,其他任何类都不可以访问它),也不可以是protected的。所以对于类的访问权限,仅有两个选择:包访问权限或public。如果不希望其他任何人对该类拥有访问权限,可以把所有构造器都指定为private,从而组织任何人创建该类的对象,但是也有例外,就是你在该类的static成员内部可以创建。


总结:本章讨论了类是如何呗构建成类库的:首先,介绍了一组类是如何被打包到一个类库中的;其次累世如何控制对其成员的访问的。控制对成员的访问权限有两个原因。第一是为了是客户不要碰触那些他们不该碰触的部分,这些部分对于类内部的操作是必要的,但是它并不属于客户端程序员所需接口的一部分。因此,将方法和于指定成private,对客户端程序员而言是一种服务。因为这样他们可以很清楚地看到什么对他们重要,什么是他们可以忽略的。这样简化了他们对类的理解。第二个原因,也是最重要的原因,是为了让类库设计者可以更改类的内部工作方式,而不必担心这样会对客户端程序员产生重大影响。