Thinking in Java学习杂记(5-6章)

Java中可以通过访问控制符来控制访问权限。其中包含的类别有:public, “友好的”(无关键字), protected 以及 private。在C++中,访问指示符控制着它后面所有定义,直到又一个访问指示符加入为止,而在Java中,每个访问指示符都只控制着对那个特定定义的访问。

为Java创建一个源码文件时,它通常叫做一个“编译单元”(有时也叫做“翻译单元”)。每个编译单元都必须有一个以.java结尾的名字。而且在编译单元内部,可以有一个公共(public)类,它必须拥有与文件相同的名字(包括大小写形式,但排除.java文件扩展名)。同时每个编译单元内都只能有一个public类,其它的类可在包外部进行隐藏,因为它们是非“公共”的。

在编译.java文件时,我们会获得一个名字完全相同的输出文件;但对于.java文件中的每个类,它们都有一个.class扩展名。一个有效的程序就是一系列.class文件,它们可以封装和压缩到一个JAR文件里。Java解释器负责对这些文件的寻找、装载和解释。

注:Java并没有强制一定要使用解释器。一些固有代码的Java编译器可生成单独的可执行文件。

Java解释器的工作程序如下:首先,它找到环境变量CLASSPATH。CLASSPATH包含了一个或多个目录,它们作为一种特殊的“根”使用,从这里展开对.class文件的搜索。从那个根开始,解释器会寻找包名,并将每个点号替换成一个斜杠,从而生成从CLASSPATH根开始的一个路径名(如package foo.bar.baz会变成foo/bar/baz或foo\bar\baz)。随后将它们连接到一起,成为CLASSPATH内的各个条目(入口)。以后搜索.class文件时,就可以从这些地方开始查找准备创建的类名对应的名字。此外,它也会搜索一些标准目录。

自动编译

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

Java访问指示符

如果根本不指定访问指示符,则为默认访问,它通常称为“友好”访问。这意味着当前包内的其他所有类都能访问“友好”的成员。但对包外的所有类来说,这些成员却是“私有”的,外界不得访问。由于一个编译单元只能从属于单个包,所以单个编译单元内的所有类相互间都是自动“友好”的。因此,我们也说友好元素拥有“包访问”权限。

为了获得对一个类成员的访问权限,唯一的方法就是:

  1. 使成员成为"public"。这样所有人从任何地方都可以访问它
  2. 变成一个“友好”成员,方法是舍弃所有访问控制符,并将其类置于相同的包内。这样一来,其他类就可以访问成员
  3. 一个继承的类既可以访问一个protected成员,也可以访问一个public成员(但不可以访问private成员)。只有两个类位于相同的包内时,它才可以访问友好成员
  4. 提供“访问器/变化器”方法(亦称“获取/设置”方法),以便读取和修改值。

对于private关键字,除非是那个特定的类或者该类的方法中,否则没有人能够访问private关键字修饰的成员。

当使用private对默认构造器进行定义时,可以防止对这个类的继承。

我们使用extend关键字来继承基类。语法如:
Class A extend B{}

其中A将成为B的衍生类。在衍生器的构建器中,Java会自动插入对基础类构建器的调用,当然,只会调用默认构建器,如果需要使用基类的含参构建器,则需要通过super(参数列表)来进行调用。同时,在衍生类进行初始化的时候,将首先对其基类进行初始化。需要注意的是,当基础类中不包含无参构建器时,需要在衍生类的构建器的第一行进行基类构建器的指定。

public class Test {
    public static void main(String[] args) {
        B b = new B();
    }
}

class A{
    public A(){
        System.out.println("construct A");
    }
}

class B extends A{
    public B(){
        System.out.println("construct B");
    }
}
// output
/*
construct A
construct B
*/
public class Test {
    public static void main(String[] args) {
        B b = new B("hello");
    }
}

class A{
    public A(String a){
        System.out.println("construct A with param " + a);
    }
}

class B extends A{
    public B(String b){
        super(b);
        System.out.println("construct B");
    }
}
// output
/*
construct A with param hello
construct B
*/

许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面:

  1. 编译期常数,它永远不变
  2. 在运行期初始化一个值,我们不希望它发生变化

对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程中。也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销。在Java中,这些形式的常数必须属于基本数据类型,而且要用final关键字进行表达。在对这样的一个常数进行定义时,必须给出一个值。并且对于final字段,其储存的一个数据是不得改变的。

对于基本数据类型,final会将值变成一个常数,但对于对象句柄,final会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。

对于自变量来说,如果我们使用final进行修饰,则不能在之后对其值进行修改,而如果是一个方法(函数)被我们使用final进行修饰,则该方法的行为在继承期间将会保持不变,而且不可被覆盖或改写。如果我们希望一个类不能被继承,我们同样也可以使用final进行修饰。

在同时有static且进行继承时,程序的初始化顺序为:装载程序首先会注意到它的基础类,随后将之载入。无论是否准备生成那个基础类的一个对象,这个过程都会发生。若基础类中含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类执行static初始化,再在下一个衍生类执行,以此类推。此时,所有必要的类已经装载完毕,所以能够创建对象。首先这个对象中的所有基本数据类型都会设成它们的默认值,而将对象句柄设置为null。随后会调用基础类构建器。基础类的构建采用与衍生类构建器完全相同的处理过程。基础类构建器完成之后,实例变量会按本来的顺序得以初始化。最后,执行构建器剩余的主体部分。

Thinking in Java学习杂记(1-4)
Thinking in Java学习杂记(第7章)

posted @ 2020-03-31 19:26  范中豪  阅读(255)  评论(0编辑  收藏  举报