初识Java 5-1 实现隐藏

目录

库单元:package

代码组织

独一无二的包名

Java访问权限修饰符

包访问权限

接口访问权限(public)

不可访问(private)

继承访问权限(protected)

包访问权限与公共构造器

接口与实现

类的访问权限

新特性:模块


 

本笔记参考自: 《On Java 中文版》


        对于一些项目而言,大部分的时间和金钱并不是消耗在编程阶段,而是消耗在了之后的维护阶段。重构存在的一个主要原因,就是为了重写那些可以正常工作的代码,提高其的可读性、可理解性和可维护性。

        在实际进行开发的过程中会发现,有些库的用户希望能依赖于他们目前使用的那部分代码,而库的创建者却要求能自由修改和完善自己的代码。为了解决这一矛盾,Java提供了访问权限修饰符,包括:publicprotected、包内访问(默认权限,无关键字)private。在此之上,为了能够将类进行归纳、打包,Java还提供了package关键字。

库单元:package

        一个包内含了一组类,这些类由同一个命名空间组织在一起。例如,使用Java提供的类ArrayList,该类就被放置在命名空间java.util中,可以通过两种方式进行用:

// 方法一:指定全名
java.util.ArrayList list = new java.util.ArrayList();

        而为了使代码看起来更加简洁,可以使用import关键字:

// 方法二:使用import关键字
import java.util.ArrayList;

public class SingleImport {
    public void main(String[] args) {
        ArrayList list = new ArrayList(); // 可直接使用
    }
}

    也可以使用“*”导入java.util的其他类:import java.util.*;

        上述的这种导入提供了一种管理密码空间的机制:通过完全控制Java中的命名空间,为每一个类创建唯一的标识符组合(所以,包名的设计也是需要经过考虑的。)

        一个Java源代码文件就是一个编译单元(又称转译单元)。对一个编译单元,有几点限制:

  1. 每个编译单元中只能有一个public类;
  2. public类的名字必须与文件同名(包括大小写,但不包括文件扩展名)。

        若一个编译单元有除了public类之外的其他类,那么在该编译单元所处的包外是无法发现这些类的,因为这些类只是public类的支持类。

代码组织

        编译一个.java文件时,文件中的每一个类都会生成一个输出文件(扩展名是.class)。输出文件的名字就是其在.java文件中对应类的名字。这些.class文件可以通过jar归档器打包成一个Java档案文件(JAR),JAR通过Java解释器进行使用。

        库,就是一组如上所述的类文件(.class文件)。每个源文件通常都由一个public类和任意数量的非public类组成,因此每个源文件中都有一个公共组件。可以使用package关键字把这些组件打包到一个命名空间中

        package语句必须出现在文件的第一个非注释处,形如:

package bagname; // bagname就是包名,在本地中可以理解为文件所处的文件夹的名字

        上述语句将一个编译单元包括到了bagname这个库中。同时,这个编译单元中如果存在public类,那么要调用这个类,就必须先使用bagname这一命名空间。

        现在,假设在一个项目中存在着文件MyClass.java,其存在于路径test/hiding/mypackage中:

package hiding.mypackage;

package hiding.mypackage;

public class MyClass {
    private static MyClass m;

    MyClass() {
        // ...
    }

    public static MyClass Make() {
        System.out.println("Hello, MyClass test");
        return m;
    }
}

        若要使用MyClass,就必须使用import关键字使得example.mypackage中的名称可用。例如:

// 位于文件夹test/hiding中
package hiding;
import hiding.mypackage.*; // [1]

public class QualifiedMyClass {
    public static void main(String[] args) {
        MyClass m = new MyClass(); // [1]
        // example.mypackage.MyClass m = new example.mypackage.MyClass(); // [2]
    }
}

        其中,[1]和[2]使用其一即可。Java通过packageimport关键字来分隔命名空间,防止冲突。

    此处将QualifiedMyClass.java文件放入了hiding包内。若不进行打包,则在进行编译或运行时,可能会遇到NoClassDefFoundError错误。另外,进行打包后,类的全称会发生改变,例如MyClass的全称就是hiding.mypackage.MyClass(若使用java指令运行MyClass时,需要使用全称,并且建议在test文件夹中运行)。


独一无二的包名

        为了整合包所有的组件,一个好的办法是将属于该包的所有.class文件放到一个目录中(利用好操作系统的分层结构)。这种方式解决了两个问题:①创建唯一的包名;②寻找可能隐藏在目录结构中的类。

        按照惯例,package的名称通常由两个部分组成:

  1. 第一部分,由创建者的反向的因特网域名构成;
  2. 第二部分,由机器上的目录组成。

        例如,将mp.csdn.net进行颠倒,就会得到net.csdn.mp。通过使用net.csdn.mp,我们就可以得到一个用于唯一认定自己的类的全局名称。现在可以创建一个simple库,就可以进一步细分,得到以下的包名:

package net.csdn.mp.simple;

        像这种的包名就可以被用作命名空间,用来保护位于其中的类:

// 包名的第二部分就是 net.csdn.mp.simple
// 创建一个包
package net.csdn.mp.simple;

public class Vector {
    public Vector() {
        System.out.println("这个类的全名应该是:net.csdn.mp.simple.Vector");
    }
}

        这个组件可以通过下列语句进行引用:

import net.csdn.mp.simple.Vector;

        假设上述的Vector.java源文件在本地中储存的位置是:

/home/user/Java/test/net/csdn/mp/simple

        上述路径的后半部分net.csdn.mp.simple组成了包名。而路径的第一部分则由CLASSPATH环境变量提供。为此,就需要在本地机器中,为CLASSPATH添加路径(由于笔者使用的是Ubuntu系统,故提供当前系统下添加路径的方法)

export CLASSPATH=/home/user/Java/test

        进行这种类型的路径添加,一个主要的好处是可以在当前的包外使用自定义的库。例如,当添加了上述路径时,可以在与test平行的目录other中,使用MyClass类:

// 位置为:other/HelloWorld.java

import net.csdn.mp.simple.Vector;

public class HelloWorld {
    public static void main(String[] args) {
        Vector v = new Vector();
    }
}

        可以运行程序,得到下列输出:

冲突

        若通过*导入的两个库中包含了相同的名称,例如:

import net.csdn.mp.simple.*; // 包含了一个自定义的Vector类
import java.util.*; // 包含了标准库中的Vector类

        此时,若按照如下方式创建Vector类,就有可能发生冲突:

Vector v = new Vector();

        尝试编译,发生报错:

        正确的方式是对需要使用的Vector类进行指定

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

        因此,在这种情况下就不需要通过import关键字进行导入,除非还使用了库中的其他内容。或者,如果使用单类导入的形式,也可以避免发生冲突。

    Java不存在C语言拥有的条件编译,这是因为Java能够自动跨平台。但有时,为了进行调试,还是需要使用条件编译的,为此可以通过package关键字改变导入的包,将程序中使用的代码从调试版本切换到生产版本,以此实现条件编译的功能。

Java访问权限修饰符

        Java的访问权限修饰符包括:publicprotectedprivate。这些修饰符放在类中成员(包括字段和方法)定义的前面,控制被修饰定义的访问。而若不使用访问权限修饰符,成员会拥有默认的“包访问权限”。

包访问权限

        默认访问权限没有关键字,通常称为包访问权限(又称“友好访问权限”)。这种权限只允许当前包中的所有其他类访问该成员,而对此包之外的所有类,该成员显示为private(处于隐藏状态)。由于一个编译单元只属于一个包,所以一个编译单元中的所有类都可以通过包访问权限进行相互访问。

        包访问权限的存在,要求将类分组到一个包中。所以,在Java中,以合理的方式组织文件中的定义方式是重要的。

        类控制着那些代码可以访问其成员。授予成员访问权限的几个方法如下所示:

  1. 将成员设置为public,这样就允许任意代码访问该成员;
  2. 不为成员添加任何访问权限修饰符——赋予成员包访问权限。这样处于同一个包内的其他类就可以访问该成员;
  3. 当使用继承时,子类可以访问父类的protected成员和public成员,若两个类处于同一个包中,子类可以进一步访问父类的包访问权限成员;
  4. 提供可以读取和更改值的访问器(accessor)和修改器(mutator)方法。

接口访问权限(public)

        若使用public修饰,这意味着其之后的成员对所有人而言都是可用的,包括开发者和客户。假设存在一个包含了以下编译单元的desert包:

// 位于目录 /example/hiding/desert 中
package hiding.desert;

public class Cookie { // 具有public权限,可从包外进行访问
    public Cookie() {
        System.out.println("这是一个类Cookie的构造器");
    }

    void bite() { // 默认的包访问权限,无法从包外进行访问
        System.out.println("随便写点什么");
    }
}

    记得将hiding目录设置为CLASSPATH指定的路径之一。使用语句javac -classpath ./ xxx.java可以在编译时将CLASSPATH设定到当前目录。

        现在可以让其他程序使用类Cookie了:

// 位于example/中
package example;

// 位于目录 /hiding 中
import hiding.dessert.*;

public class EatingTheFood {
    public static void main(String[] args) {
        Cookie c = new Cookie();
    }
    // c.bite(); // 无法访问
}

        程序运行的结果如下:

        在上述的Cookie类中,由于其的构造器和类都是public的,所以它的对象可被创建。但方法bite()只有包访问权限,所以在其包外是无法进行访问的。

默认包

        若两个类处于同一目录中,那么即使这两个类没有明确的包名,它们也可以进行相互的访问。例如:

// 位置是hiding/Cake.java
class Cake {
    public static void main(String[] args) {
        Pie x = new Pie();
        x.f();
    }
}

        下面的文件Pie.javaCake.java处于同一目录中:

// 位置是hiding/Pie.java
class Pie {
    void f() {
        System.out.println("这条语句存在于Pie.java中");
    }
}

        被放在同一目录中的类会被视为属于当前目录的“默认包”的隐含部分。因此,这种文件会为该目录中的其他文件提供包访问权限。


不可访问(private)

        若一个类存在使用private修饰的成员,那么除了包含此成员的类及其成员之外,其他任何类不可访问该成员(即使是同一个包中的其他类)。通常,可以自由修改和替换通过private修饰的成员(虽然隐藏可以通过反射进行规避)

    特别是在涉及多线程时,使用private是十分重要的。

class Sundae {
    private Sundae() { // 该构造器被隐藏,无法从Sundae外被调用
        System.out.println("一个被隐藏的Sundae构造器");
    }

    static Sundae makeASundae() {
        return new Sundae(); // 在Sundae类中,拥有调用构造器Sundae()的权限
    }
}

public class IceCream {
    public static void main(String[] args) {
        Sundae x = Sundae.makeASundae(); // 通过调用静态方法,可以通过指定构造器进行对象创建
    }
}

        程序运行的结果如下:

        上述程序展示了private的一个用法:控制对象的创建方式,防止特定的构造器(或所有的构造器)被调用。

    若确定某方法是类的“辅助”方法,将其设为private能够为后期的维护与修改保留选择。字段也是如此,除非需要公开底层实现,否则最好将字段设为private


继承访问权限(protected)

        protected关键字多被用于处理继承的概念。通过继承,可以通过一个现有类(即基类),在不修改现有类的情况下向类中添加新成员,或改变现有成员的行为。使用extends声明新类继承了现有类:

class SubClass extends BaseClass { // ...

        子类对基类的访问权限有几种:

  1. 子类可以访问基类的public成员和protected成员;
  2. 若子类和基类位于同一个包中,子类可以访问基类所有的包访问权限成员。

        protected关键字也提供了包访问权限。其与public的区别在于,若不在同一个包中,那么包外的类可以访问public成员,但是不可以访问protected成员。

        以之前提到的Cookie.java为例,它的bite()方法只有包访问权限:

void bite() { // 默认的包访问权限,无法从包外进行访问
    System.out.println("随便写点什么");
}

        但如果有子类需要访问该方法,那么就必须改变bite()的权限。public允许所有的访问,这或许不会是我们想要的。为此,就需要使用protected关键字了:

// 位于目录 /hiding/dessert 中
package hiding.dessert;

public class Cookie { // 具有public权限,可从包外进行访问
    public Cookie() {
        System.out.println("这是一个类Cookie的构造器");
    }

    protected void bite() { // 默认的包访问权限,无法从包外进行访问
        System.out.println("随便写点什么");
    }
}

        这样就可以让任何继承Cookie的类访问bite()了:

// 位置是hiding/ChocolateChip.java
package hiding;

import hiding.dessert.Cookie;

public class ChocolateChip extends Cookie {
    public ChocolateChip() {
        System.out.println("这是ChocolateChip的构造器");
    }

    public void chomp() {
        bite(); // 使用了Cookie的protected方法
    }

    public static void main(String[] args) {
        ChocolateChip c = new ChocolateChip();
        c.chomp();
    }
}

        程序运行的结果是:


包访问权限与公共构造器

        若一个类只具有包访问权限,现在给这个类一个public构造器,会发现编译器不会进行任何报错:

// 位置是hiding/packageaccess/PublicConstructor.java
package hiding.packageaccess;

class PublicConstructor {
    public PublicConstructor() { // ...
    }
}

        但这不代表没有问题,因为这是一个虚假陈述——实际上无法从包外访问这个public构造器。如果尝试从外部对其进行调用,就会发现报错:

// 位置是hiding/CreatePackageAccessObject.java
package hiding;

import hiding.packageaccess.*;

public class CreatePackageAccessObject {
    public static void main(String[] args) {
        new PublicConstructor();
    }
}

        发生了报错:

接口与实现

        访问控制也被称为实现隐藏。将数据方法包装在类中,并与实现隐藏相结合,称为封装。其结果就是具有特征和行为的数据类型。

        访问权限控制在数据类型的内部设置了访问边界:

  1. 确定客户程序员可以使用和不可使用的内容;
  2. 将接口和实现分离。

类的访问权限

        访问权限修饰符还决定了库内部的那些类可以提供给用户使用。要控制对类的访问,访问权限修饰符就必须出现在关键字class前面:

public class Weight { // ...

        这种类的使用之前已经多次进行过展示,这里就不再赘述了。值得一提的是,在进行类的设计时会额外添加一些限制:每个编译单元(即文件)都只能有一个public,且该类的名字必须和文件名完全一致。

        当然,编译单元中可以没有public类。这种类通常都是用来完成一些其他public类分发的任务,不使用public可以让这种类隐藏在包中,这样可以让实现变得更加灵活。

        关于赋予权限,有这样的一些建议:

  • 应该尽量将字段设置为private权限;
  • 方法应该具有和类相同的访问权限(包访问权限);
  • 类不应该是privateprotected的,类的访问权限有两种:包访问权限和public。若想防止对该类的访问,可以将其构造器全部设置为private(转而使用静态方法创建对象)。
class Soup1 {
    private Soup1() { // 不允许外部访问构造器
        System.out.println("隐藏的Soup1构造器");
    }

    public static Soup1 makeSoup() {
        return new Soup1(); // 使用静态方法返回对象
    }
}

class Soup2 {
    private Soup2() {
        System.out.println("隐藏的Soup2构造器");
    }

    private static Soup2 s2 = new Soup2(); // 声明静态对象

    public static Soup2 access() {
        return s2;
    }

    public void f() {
        System.out.println("被Soup2对象调用的f()");
    }
}

public class Lunch {
    void testPrivate() {
        // Soup1 soup1 = new Soup1(); // 编译报错,Soup1是隐藏的
    }

    void testStatic() {
        Soup1 s = Soup1.makeSoup();
    }

    void testSingleon() {
        Soup2.access().f();
    }

    public static void main(String[] args) {
        Lunch lh = new Lunch();
        lh.testStatic();
        lh.testSingleon();
    }
}

        程序执行的结果如下:

        上述程序中,类Soup2使用的是单例模式,从始至终只创建一个对象。

新特性:模块

        在JDK 9之前,Java程序的运行会依赖整个Java库。这意味着,哪怕只是使用库的一个组件,编译器也会将整个Java库包含在里面。

        JDK 9引入了模块的概念:将代码划分为一个一个的模块。由这些模块指定它们依赖的模块,并且定义可用或者不可用的模块。在此之后,若使用库组件,就只会获得对应的模块及其的依赖项,这就省去了不必要的导入。

     另外,使用“逃生舱口”(escape hatch)可以调用隐藏的库组件,但因此产生的可能问题需要由程序员自己负责。

        为了更好地探索这个新的系统,Java提供了新的命令行标识,比如:

  1.  显示所有可用模块:
    java --list-modules
  2. 若要查看模块的内容,例如base模块,可以使用命令:

    java --describe-module java.base

    模块系统多被用于大型的第三方库。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值