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

访问控制(或隐藏具体实现)与“最初的实现并不恰当”有关;

把一个代码放在某个为止,等过一会儿回头再看时,有可能会发现有更好的方式实现相同的功能,这是重构的原动力之一,重构即重写代码,以使它更可读、更易理解,并因此更具有可维护性。

但是,通常会有一些消费者需要你的代码在某些方面保持不变。因此,你想改变代码,而他们却想让代码保持不变。由此产生了面向对象设计中需要考虑的一个基本问题:如何把变动的事物与保持不变的事物区分开来。

这对类库而言尤为重要,类库的消费者必须以来它所使用的那部分类库,并且能够知道如果类库出现了新版本。它们并不需要改写代码。从另一个方面来说,类库的开发者必须有权限进行修改和改进,并确保客户代码不会因为这些改动而受到影响

类库的开发者必须保证在改动类库中的类时不得删除任何现有方法,因为那样会破坏客户端程序员的代码。在有域存在的情况下,类库开发者要怎样才能知道究竟都有哪些域已经被客户端程序员所调用了呢?这对于方法仅为累的实现的一部分,因此并不想让客户端程序员直接使用的情况来说同样如此。如果程序开发者想要移除旧的实现而要添加新的实现时,结果将会怎样呢?改动任何一个成员都有可能破坏客户端程序员的代码。

为解决这一问题,Java提供了访问权限修饰词,供类库人员向客户端程序员指明哪些是可用的,哪些是不可用的。访问权限控制的等级,从最大权限到最小权限依次为:public,protected,包访问权限和private。正确的做法是:尽可能将一切方法都定为private,而仅向客户端程序员公开你愿意让它们使用的方法。

  • 包:库单元
  • Java访问权限修饰词
  • 接口和实现
  • 类的访问权限

1.包:库单元

包的作用是将构建捆绑到一个内聚的类库单元中,使用关键字package加以控制,而访问权限修饰词会因类是存在于一个相同的包,还是存在于一个单独的包而受到影响。

包内包含有一组类,它们在单一的名字空间之下被组织在了一起。

例如,在Java的标准发布中有一个工具库,它被组织在java.util名字空间之下。java.util中有一个叫做ArrayList的类,使用ArrayList的一种方式是用其全名java.util.ArrayList来指定。

这样写,会使代码变得冗长,解决方法是使用import关键字,可以导入一个类:

现在,就可以不用限定地使用ArrayList了,但是这里只能使用ArrayList类。若想导入java.util中的其他类,可以使用*,如下:

现在可以使用java.util中的所有类了

之所以要导入,就是要提供一个管理名字空间的机制。所有类成员的名称都是彼此隔离的。A类中的方法f()和B类中的方法f()不会彼此冲突,但是若类名称相互冲突又该如何解决?

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

代码组织:

当编译一个.java文件时,在.java文件中的每个类都会有一个输出文件,而该输出文件的名称与.java文件中每个类的名称相同,只是多了一个后缀.class。因此,在编译少量.java文件之后,会得到大量的.class文件。Java可运行程序是一组可以打包并压缩为Java文档文件(JAR,使用Java的jar文档生成器)的.class文件。Java解释器负责这些文件的查找,装载和解释。

类库实际上是一组类文件,其中每个文件都有一个public类,以及任意数量的非public类。因此每个文件都有一个构件。如果希望这些构件(每一个都有它们自己的独立的.java和.class)从属于同一个群组,就可以使用关键字package。

如果使用package语句,它必须是除注释外的第一句程序代码:

表示该编译单元是名为access的类库的一部分。或者说,正在声明该编译单元中的public类名称是位于access名称的保护伞下。任何想要使用该名称的人都必须使用前面给出的选择,指定全名或者与access结合使用关键字import。(Java包的命名全部使用小写字母,包括中间的也是如此)

例如:假设文件的名称是MyClass.java,这就意味着在该文件中有且只有一个public类,该类的名称必须是MyClass:

 

现在如果有人想用MyClass或者是access中的任何其他public类,就必须使用关键字import来使用access中的名称,另一个选择是给出完整的名称:

使用import可使使用更加简单:

 

注意:package和import关键字允许你做的,是将单一的全局名字空间分割开,使得无论多少人使用Internet以及Java开始编写类,都不会出现名称冲突问题。

创建独一无二的包名:

既然一个包从未真正将被打包的东西包装成单一的文件,并且一个包可以由许多.class文件构成,那么情况就有点复杂了。为避免这种情况的发生,一种合乎逻辑的做法就是将特定包的所有.class文件都置于一个目录下。这是利用操作系统的层次化的文件结构来解决这一问题。

将所有的文件收入一个子目录还可以解决两个问题:

1).创建独一无二的名称;

2).查找有可能隐藏于目录结构中某处的类。

这些任务通过将.class文件所在的路径位置编码成package的名称来实现。

package名称的第一部分是类的创建者的反顺序的Internet域名。因为域名唯一,所以package包名也会是唯一的。

第二部分是把package名称分解为你机器上的一个目录。所以当Java程序运行并且需要加载.class文件的时候,它就可以确定.class文件在目录上所处的位置。

Java解释器的运行过程:

首先,找出环境变量CLASSPATH。CLASSPATH包含一个或多个目录,用作查找.class文件的根目录。从根目录开始,解释器获取包的名称并将每个句点替换成反斜杠,以从CLASSPATH根中产生一个路径名称。得到的路径会与CLASSPATH中的各个不同的项相连接,解释器就在这些目录中查找与你所要创建的类的名称相关的class文件

在使用外部JAR文件时,必须在类路径中将JAR文件的实际名称写清楚,而不仅是指明它所在位置的目录。对于grape.jar文件:

一旦类路径得以正确建立,下面的文件就可以放于任何目录之下:

当编译器碰到simple库的import语句时,就开始在CLASSPATH所指定的目录中查找,查找子目录net/mindview/simple,然后从已编译的文件中找出名称相符者(对vector而言是Vector.class,对List而言是List.class)。这里Vector和List中的类以及要使用的方法都必须是public的。

冲突

如果将两个含有相同名称的类库以"*"形式同时导入,将会出现什么情况呢?

由于java.util.*也含有一个Vector类,这就存在潜在的冲突。但是只要不写那些导致冲突的程序代码,就不会有什么问题;

但是如果现在需要创建一个Vector类,就会产生冲突:

 

这行到底取用的是哪个Vector类?编译器不知道,就会抛出异常。如果想要一个标准的Java Vector类,就得写:

这样可以完全指明该Vector类的位置。

在使用了有冲突名字的情况下,必须返回到指定全名的方式。 

定制工具库:

具备上面的知识就可以构建自己的工具库来减少或消除重复的程序代码了。

这个文件的位置一定是在某个以CLASSPATH位置开始,然后接着是net/mindview的目录下。编译完之后,就可以用import static语句在自己的系统上使用静态的print()和printnb()方法。

 结果: 

该类库的第二个构件可以是在第4章中引入的range()方法,它使得foreach语法可以用于简单的整数序列:

 

用import改变行为:

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

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

对使用包的忠告:

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

2.Java访问权限修饰词

public、protected和private这几个Java访问权限修饰词在试用时,是置于类中每个成员的定义之前的---无论它是一个域还是一个方法。每个访问权限修饰词仅控制它所修饰的特定定义的访问权。

如果不提供任何访问权限修饰词,则意味着它是“包访问权限”

包访问权限:

默认访问权限没有关键字,通常默认为包访问权限(有时表示为friendly)。这意味着当前的包中的所有类对那个成员都有访问权限,但对于这个包之外的所有类,这个成员却是private。由于一个编译单元,只能隶属于一个包,所以经由包访问权限,处于同一个编译单元中的所有类彼此之间都是自动可访问的。

包访问权限允许将包内所有相关的类组合起来,以使它们彼此之间可以轻松地相互作用。当把类组织起来放进一个包内之时,也就给它们的包访问权限的成员赋予了相互访问的权限。

取得对某成员的访问权的唯一途径是

1).使该成员成为public,于是,无论是谁,无论在哪里,都可以访问该成员

2).通过不加访问权限修饰词并将其它类放置于同一个包内的方式给成员赋予包访问权。这样,包内的其他类也就可以访问该成员了

3).继承而来的类即可以访问public成员也可以访问protected成员(private成员是不能被访问的)。只有在两个类都处于同一个包内时,它才可以访问包访问权限的成员。

4).提供访问器(accessor)和变异器(mutator)方法(也称作get/set方法),以读取和改变数值。对OOP而言,get/set方法是最优雅的方式。

public:接口访问权限

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

Cooki.java必须位于名为dessert的子目录中,该子目录在access下,而c05则必须位于CLASSPATH指定的众多路径的其中之一的下边。不要错误的认为Java总是将当前目录视作查找行为的起点之一,只有当CLASSPATH之中存在一个"."作为路径之一,Java才会查找当前路径。

如果创建了一个使用Cookie的程序:

就可以创建一个Cookie对象,因为它的构造器是public而且类也是public的,但是bite()只向在dessert包中的类提供访问权,所以bite()成员在Dinner.java之中是无法访问的,因此编译器也禁止你使用它,必须使用Cookie类的对象进行调用。

默认包

看下面两个程序:

和程序二:

第一个程序看似破坏了上述规则,但它仍然可以编译;第二个程序处于相同目录的文件中。

Cake.java可以访问Pie和f()的原因是它们处于相同的目录并且没有给自己设定任何包名称

Java将这样的文件自动看作是隶属于该目录的默认包之中,于是它们为该目录中所有其他的文件都提供了包访问权限。 

private:你无法访问

 private,除了包含该成员的类之外,其他任何类都无法访问这个成员。由于处于同一个包内的其他类是不可以访问private成员的,这相当于自己隔离了自己。

默认的包访问权限通常已经提供了充足的隐藏措施,使用类的客户端程序员是无法访问包访问权限成员的。

private示例:

 

这里private用于控制如何创建对象,并阻止别人直接访问某个特定的构造器(或全部构造器)。在这个例子中,不能通过构造器来创建Sundae对象,而必须使用makeASundae()方法来达到此目的。

任何可以肯定只是该类一个"助手"方法的方法,都可以把它指定为private,以确保不会在包内的其他地方误用到它,于是,也防止了使用者取改变或者删除这个方法。将方法指定为private确保你拥有这种选择权。

这对于类中的private域同样适用。除非必须公开底层实现细目,否则就应该将所有的域指定为private。

protected:继承访问权限

 关键字protected处理的是继承的概念,通过继承可以利用一个现有类---将其称为基类,然后将新成员添加到该现有类中而不必碰该现有类。还可以改变该类的现有成员的行为。继承使用extends(扩展),就像:

 

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

如果创建了一个新包,并自另一个包中继承类,那么唯一可以访问的成员就是源包的public成员。(如果在同一个包内执行继承工作,就可以操纵所有的拥有包访问权限的成员)。

有时,基类的创建者会希望有某个特定成员,把对它的访问权限赋予派生类而不是所有类。这就需要protected来完成这一工作。protected也提供包访问权,也就是说,相同包内的其他类可以访问protected元素。

这里碰到一个问题,类Cookie中存在一个方法bite(),该方法也存在于任何一个从Cookie继承而来的类中,但bite()是包访问权限,且位于另一个包内,所以这个包无法使用。解决方法可以将其定义为public,但这样的话,所有的人都可以访问它;另一个方法是使用protected权限:

将bite()方法指定为protected权限,现在继承Cookie就可以使用bite()方法了:

 

3.接口和实现

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

为何将权限控制的边界划在数据类型的内部?

1).设定客户端程序员可以使用和不可以使用的界限,可以在结构中建立自己的内部机制,而不必担心客户端程序员会偶然地将内部机制当作是它们可以使用的接口的一部分。

2).将接口和具体实现进行分离。如果结构是用于一组程序之中,而客户端程序员除了向接口发送信息之外什么也不可以做的话,那么就可以随意更改所有不是public的东西,而不会破坏客户端代码。

为了清楚起见,将public成员置于开头,后面跟着protected、包访问权限和private成员的创建类的形式。 

4.类的访问权限

 在Java中,访问权限修饰词也可以用于确定库中的哪些类对于该库的使用者是可用的。如果希望某个类可以为某个客户端程序员所用,就可以通过关键字public作用域整个类的定义来达到目的。这样做可以控制客户端程序员是否能创建一个该类的对象。

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

 

这里存在一些额外的限制:

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

2).public类的名称必须完全与含有该编译单元的文件名相匹配,包括大小写。所以对于Widget而言,文件的名称必须是Widget.java。

3).虽然不是很常用,但编译单元内完全不带public类也是可能的。在这种情况下,可以随意对文件命名

在创建一个包访问权限的类时,仍旧是在将该类的域声明为private时才有意义--应尽可能的将域指定为私有的,但是通常来说,将与类(包访问权限)相同的访问权限赋予方法也是合理的。

需要注意,类既不可以是private也不可以是protected的。所以对于类的访问权限,仅有两个选择:包访问权限或public。如果不希望其他任何人对该类拥有访问权限,可以把所有的构造器都指定为private,从而组织任何人创建该类的对象,但是有一个例外,可以在该类的static成员内部创建。比如:

 

这里,Soup1类和Soup2类展示了如何通过将所有的构造器指定为private来组织直接创建某个类的实例。如果没有明确地至少创建一个构造器的话,就会帮你创建一个默认构造器(不带任何参数的构造器)。若将构造器设定为private,那么就谁也无法创建该类的对象了,上面的方法中给出了拉你各种使用这种类的方法:

1).在Soup1中,创建一个static方法,它创建一个新的Soup1对象并返回一个对它的引用。这种做法的好处是可以很方便的统计创建了多少个Soup1对象,还可以方便在引用Soup1之前做一些额外的工作。

 2).Soup2用到了设计模式,这种模式叫做单例设计模式,这是因为你始终只能创建它的一个对象。Soup2类的对象是作为Soup2的一个static private成员而创建的,所以有且仅有一个,而且除非是通过public方法access(),否则无法访问到它的。

注意:相同目录下所有不具有明确package声明的文件,都被视作是该目录下默认包的一部分

posted @ 2019-07-22 21:53  ifreewolf  阅读(557)  评论(0编辑  收藏  举报