Thinking in Java
原书中文翻译有点烂,进行了一些小小的改动。略过一些无关紧要的,比如说运算符、控制流程等。
关键词:static、package、public、protected、private、数组等
下一篇:第六章 https://blog.youkuaiyun.com/mooe1011/article/details/88067724
通常,我们创建类时只有执行了new 后,才会正式生成数据存储空间,并可使用相应的方法。
但在两种特殊的情形下,上述方法就不行了。一种情形是只想用一个存储区域来保存一个特定的数据。另一种情形是即使没有创建对象,也需要一个能调用的方法。为满足要求,可使用static(静态)关键字。一旦设为static,数据或方法就不会同那个类的任何对象实例联系到一起
class StaticTest {
Static int i = 47;
}
尽管制作了两个StaticTest 对象,但它们仍然只占据StaticTest.i的一个存储空间。
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
此时,无论 st1.i还是 st2.i都有同样的值 47,因为它们引用的是同样的内存区域。
StaticTest.i++;
++运算符会使变量增值。此时,无论 st1.i 还是st2.i 的值都是48。
static void incr() { StaticTest.i++; }
从中可看出,StaticFun 的方法 incr()使静态数据 i增值。通过对象,可用典型的方法调用incr():
StaticFun sf = new StaticFun();
或者,由于 incr()是一种静态方法,所以可通过它的类直接调用:
假定我们在一个方法的内部,并希望获得当前对象的句柄。由于那个句柄是由编译器“秘密”传递的,所以没有标识符可用。然而有个专用的关键字:this。this 关键字(注意只能在方法内部使用)可为已调用了其方法的那个对象生成相应的句柄。可象对待其他任何对象句柄一样对待这个句柄。但要注意,假若准备从自己某个类的另一个方法内部调用一个类方法,就不必使用this。只需简单地调用那个方法即可。当前的this 句柄会自动应用于其他方法。
void pit() { pick(); /* ... */ }
System.out.println("i = " + i);
public static void main(String[] args) {
x.increment().increment().increment().print();
若为一个类写了多个构建器,那么需要在一个构建器里调用另一个构建器,可用this 关键字做到这一点。
通常,当我们说this 的时候,都是指“这个对象”或者“当前对象”。而且它本身会产生当前对象的一个句柄。在一个构建器中,若为其赋予一个自变量列表,那么 this 关键字会具有不同的含义:它会对与那个自变量列表相符的构建器进行明确的调用。如下所示:
private String s = new String("null");
System.out.println("Constructor w/ int arg only, petalCount= "+ petalCount);
System.out.println("Constructor w/ String arg only, s=" + ss);
Flower(String s, int petals) {
//! this(s); // Can't call two!
this.s = s; // 由于自变量s 的名字以及成员数据名字是相同的,可用 this.s来引用成员数据。
System.out.println("String & int args");
System.out.println("default constructor (no args)");
//! this(11); //编译器不让从除了一个构建器之外的其他任何方法内部调用一个构建器(同名)
System.out.println("petalCount = " + petalCount + " s = "+ s);
public static void main(String[] args) {
理解了this 关键字后,可更完整地理解 static方法的含义。它意味着一个特定的方法没有this。我们不可从一个 static方法内部发出对非 static方法的调用(注释②),尽管反过来说是可以的。
而且在没有任何对象的前提下,我们可针对类本身发出对一个 static方法的调用。它就好象我们创建一个全局函数的等价物(在C 语言中)。除了全局函数不允许在Java中使用以外,若将一个 static方法置入一个类的内部,它就可以访问其他static 方法以及static 字段。
②:有可能发出这类调用的一种情况是我们将一个对象句柄传到static 方法内部。随后,通过句柄(此时实际是this),我们可调用非 static方法,并访问非static 字段。但如果真的想要这样做,只要制作一个普通的、非static 方法即可。
System.out.println("Data type Inital value\n" +
public static void main(String[] args) {
Measurement d = new Measurement();
/* In this case you could also say:new Measurement().print();*/
在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,那些变量仍会在调用任何方法之前得到初始化——甚至在构建器调用之前
System.out.println("Tag(" + marker + ")");
Tag t1 = new Tag(1); // Before constructor 1
// Indicate we're in the constructor:
t3 = new Tag(33); // 在构建器里重新初始化 t3 4
Tag t2 = new Tag(2); // After constructor 2
Tag t3 = new Tag(3); // At end 先初始化这个t3 3
public class OrderOfInitialization {
public static void main(String[] args) {
t.f(); // Shows that construction is done
因此,t3句柄会被初始化两次,一次在构建器调用前,一次在调用期间
System.out.println("Bowl(" + marker + ")");
System.out.println("f(" + marker + ")");
static Bowl b1 = new Bowl(1); //1.1
System.out.println("Table()"); //1.3
System.out.println("f2(" + marker + ")");
static Bowl b2 = new Bowl(2); //1.2
static Bowl b4 = new Bowl(4); //2.1
System.out.println("Cupboard()"); //2.3->3、4
System.out.println("f3(" + marker + ")");
static Bowl b5 = new Bowl(5); //2.2
public class StaticInitialization {
public static void main(String[] args) {
System.out.println("Creating new Cupboard() in main");
System.out.println("Creating new Cupboard() in main");
static Table t2 = new Table(); //1
static Cupboard t3 = new Cupboard(); //2
Creating new Cupboard() in main
Creating new Cupboard() in main
static初始化只有在必要的时候才会进行。如果不创建一个 Table 对象,而且永远都不引用Table.b1 或Table.b2,那么b1 和b2 永远都不会创建。在创建了第一个Table 对象之后,static 对象不会重新初始化。
将其他static初始化工作划分到一个特殊的“static 构建从句”(也叫作“静态块”)里:
//............
只是一个static 关键字,后面跟随一个方法主体。与其他 static初始化一样,这段代码仅执行一次——首次生成那个类的一个对象时,或者首次访问属于那个类的一个 static 成员时:
System.out.println("Cup(" + marker + ")");
System.out.println("f(" + marker + ")");
public static void main(String[] args) {
System.out.println("Inside main()");
static Cups x = new Cups(); // (2)
static Cups y = new Cups(); // (2)
在标记为(1)的行内访问 static 对象c1 的时候,或在行(1)标记为注释,同时(2)行不标记成注释的时候,用于Cups 的 static初始化模块就会运行。
System.out.println("Mug(" + marker + ")");
System.out.println("f(" + marker + ")");
System.out.println("c1 & c2 initialized");
public static void main(String[] args) {
System.out.println("Inside main()");
实例初始化从句与静态初始化从句极其相似,只是static 关键字从里面消失了。为支持对“匿名内部类”的初始化,必须采用这一语法格式。
对于数组,初始化工作可在代码的任何地方出现,但也可以使用一种特殊的初始化表达式,它必须在数组创建的地方出现,是一系列由花括号封闭起来的值。存储空间的分配(等价于使用new),例如:
事实上在Java 中,可将一个数组分配给另一个,所以能使用下述语句:
public static void main(String[] args) {
for(int i = 0; i < a2.length; i++)
for(int i = 0; i < a1.length; i++)
prt("a1[" + i + "] = " + a1[i]);
如果不知道在自己的数组里需要多少元素,只需简单地用new在数组里创建元素。在这里,new准备创建的是一个基本数据类型的数组(不会创建非数组的基本类型):
static Random rand = new Random();
return Math.abs(rand.nextInt()) % mod + 1;
public static void main(String[] args) {
prt("length of a = " + a.length);
for(int i = 0; i < a.length; i++)
prt("a[" + i + "] = " + a[i]);
由于数组的大小是随机决定的,所以数组的创建实际是在运行期间进行的。除此以外,输出基本数据类型的数组元素会自动初始化成“空”值(对于数值,空值就是零;对于 char,它是null;而对于boolean,它却是 false)。
若是一个非基本类型对象的数组,那么必须要使用new。在这里会再一次遇到句柄问题。观察封装器类型 Integer,它是一个类,而非基本数据类型:
static Random rand = new Random();
return Math.abs(rand.nextInt()) % mod + 1;
public static void main(String[] args) {
Integer[] a = new Integer[pRand(20)];
prt("length of a = " + a.length);
for(int i = 0; i < a.length; i++) {
a[i] = new Integer(pRand(500));
prt("a[" + i + "] = " + a[i]);
Integer[] a = new Integer[pRand(20)];
它只是一个句柄数组,而且除非通过创建一个新的 Integer对象,否则初始化进程不会结束:
a[i] = new Integer(pRand(500));
若忘记创建对象,就会在运行期试图读取空数组位置时获得一个“违例”错误。
下面看看String对象的构成情况。大家可看到指向 Integer 对象的句柄会自动转换,从而产生一个String,它代表着位于对象内部的值。亦可用花括号封闭列表来初始化对象数组。
public static void main(String[] args) {
这种做法大多数时候都很有用,但限制很大,因为数组的大小是在编译期间决定的。初始化列表的最后一个逗号是可选的。
数组初始化的第二种形式(Java 1.1 开始支持)提供了一种更简便的语法,可创建和调用方法,获得与C 的“变量参数列表”一致的效果。这些效果包括未知的参数(自变量)数量以及未知的类型(如果这样选择的话)。由于所有类最终都是从通用的根类bject 中继承的,所以能创建一个方法,令其获取一个 Object数组,并象下面这样调用它:
for(int i = 0; i < x.length; i++)
public static void main(String[] args) {
f(new Object[] {new Integer(47), new VarArgs(),new Float(3.14), new Double(11.11) });
f(new Object[] {"one", "two", "three" });
f(new Object[] {new A(), new A(), new A()});
此时,对这些未知的对象并不能采取太多的操作,而且利用自动String转换对每个 Object 做一些有用的事情。在第 11章(运行期类型标识或 RTTI),大家还会学习如何调查这类对象的准确类型,来做一些有趣的事情。
第一个例子展示了基本数据类型的一个多维数组。可用花括号定出数组内每个矢量的边界:
第二个例子展示了用new分配的一个三维数组。在这里,整个数组都是立即分配的:
int[][][] a2 = new int[2][2][4];
但第三个例子却向大家揭示出构成矩阵的每个矢量都可以有任意的长度:
int[][][] a3 = new int[pRand(7)][][];
for(int i = 0; i < a3.length; i++) {
for(int j = 0; j < a3[i].length; j++)
对于第一个 new创建的数组,它的第一个元素的长度是随机的。for循环内的第二个new 则会填写元素,但保持第三个索引的未定状态——直到碰到第三个new。
这从第四个例子可以看出,它向我们演示了用花括号收集多个new表达式的能力:
{ new Integer(1), new Integer(2)},
{ new Integer(3), new Integer(4)},
{ new Integer(5), new Integer(6)},
for(int i = 0; i < a5.length; i++) {
for(int j = 0; j < a5[i].length; j++)
客户程序员必须知道一旦新版本的库更新,自己不需要改写代码。而与此相反,库的创建者必须能自由地进行修改,同时保证客户程序员代码不会受到影响。为解决这个问题,Java 推出了“访问指示符”的概念,允许库创建者声明哪些东西是客户程序员可以使用的,哪些是不可使用的。分别包括:public,“友好的”(无关键字),protected 以及private
若想导入单独一个类,可在import语句里指定那个类的名字:
现在可以自由地使用Vector类。若计划创建一个“适合在因特网使用”的程序,必须考虑如何防止类名的重复。
创建一个源码文件的时候,它通常叫作一个“编译单元”,都必须有一个以.java结尾的名字。而且在编译单元的内部,只能有一个公共(public)类,它必须拥有与文件相同的名字(包括大小写形式)。否则编译器就会报错。编译单元剩下的类(如果有的话)可在那个包外面的世界面前隐藏起来,因为它们并非public,而且它们由用于支持public类。
但对于.java 文件中的每个类都有一个.class 扩展名。一个有效的程序就是一系列.class 文件,它们可以封装和压缩到一个 JAR文件里(使用Java 1.1 提供的jar 工具)。Java 解释器负责对这些文件的寻找、装载和解释。
“库”也由一系列类文件构成。每个文件都有一个 public类(通常有一个 public 类)。如果想将所有这些组件(它们在各自独立的.java 和.class文件里)都归纳到一起,那么package 关键字就可以发挥作用。
那么package语句必须作为文件的第一个非注释语句出现。作用是指出这个编译单元属于名为mypackage 库的一部分。换句话说,它表明这个编译单元内的 public类名位于 mypackage这个名字的下面。注意根据Java 包(封装)的约定,名字内的所有字母都应小写,甚至那些中间单词亦要如此。
例如,假定文件名是MyClass.java。它意味着在那个文件有且只有一个 public类。并且那个类的名字必须是MyClass:
其他人如果想使用 MyClass,或者其他public 类,必须用import关键字激活mypackage 内的名字,或者是指定完整的名称:
mypackage.MyClass m = new mypackage.MyClass();
将package 名解析成自己机器上的一个目录。这样一来,程序运行并需要装载.class 文件的时候(这是动态进行的,在程序需要创建属于那个类的一个对象,或者首次访问那个类的一个static 成员时),它就可以找到.class 文件驻留的那个目录。
创建一个名为util 的库,我可以进一步地分割它,所以最后得到的包名如下:
System.out.println("com.bruceeckel.util.Vector");
System.out.println("com.bruceeckel.util.List");
这两个文件都置于我自己系统的一个子目录中:C:\DOC\JavaT\com\bruceeckel\util
若通过它往回走,就会发现包名com.bruceeckel.util,但路径的第一部分又是什么呢?这是由CLASSPATH环境变量决定的。在我的机器上,它是:CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT
可以看出,CLASSPATH 里能包含大量备用的搜索路径。然而,使用 JAR文件时要注意一个问题:必须将文件的名字置于类路径里,而不仅仅是它所在的路径。所以对一个名为grape.jar 的文件来说,我们的类路径需要包括:CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar
public static void main(String[] args) {
编译器遇到 import语句后,它会搜索由CLASSPATH 指定的目录,查找子目录 com\bruceeckel\util,然后查找名称适当的已编译文件(对于Vector 是Vector.class,对于 List 则是List.class)。
为导入的类首次创建一个对象时(或者访问一个类的static 成员时),编译器会在适当的目录里寻找同名的.class 文件(所以如果创建类 X的一个对象,就应该是 X.class)。然而,如果它在相同的目录中还发现了一个 X.java,编译器就会比较两个文件的日期标记。如果X.java 比X.class 新,就会自动编译X.java,生成一个最新的 X.class。
对于一个特定的类,或在与它同名的.java 文件中没有找到它,就会对那个类采取上述的处理。
若通过*导入了两个库,而且它们包括相同的名字,这时会出现什么情况呢?使用下述导入语句:
由于java.util.*也包含了一个 Vector 类,所以这会造成潜在的冲突。然而,只要冲突并不真的发生,那么就不会产生任何问题,因为否则的话,就需要进行大量编程工作,防范那些可能可能永远也不会发生的冲突:
编译器会报错,强迫我们进行明确的说明。例如,假设想使用标准的 Java Vector,那么必须象下面这样编程:
java.util.Vector v = new java.util.Vector();
由于它(与 CLASSPATH 一起)完整指定了那个Vector 的位置,所以不再需要 import java.util.*语句,除非还想使用来自java.util 的其他东西。
掌握前述的知识后,接下来就可以开始创建自己的工具库,以便减少重复的代码。例如,可为System.out.println()创建一个别名,减少重复键入的代码量。它可以是名为 tools的一个包(package)的一部分:
public static void rint(Object obj) {
public static void rint(String s) {
import com.bruceeckel.tools.*;
public static void main(String[] args) {
P.rintln("Available from now on!");
如果不指定访问指示符,通常称为“友好”(Friendly)访问。这意味着当前包内的其他所有类都能访问“友好的”成员,但对包外的所有类来说,这些成员却是Private的。
在许多语言中,在文件内组织定义的方式往往显得有些牵强。但在Java 中,却强制用一种颇有意义的形式进行组织。除此以外,有时可能想排除一些类,不想让它们访问当前包内定义的类。
一个非常重要的问题是“谁能访问我们的private 代码”。类控制着哪些代码能够访问自己的成员。另一个包内推荐可以声明一个新类,然后说:“嗨,我是Bob的朋友!”,并希望看到Bob 的“protected”、友好的以及“private”的成员。为获得访问权限,方法是:
(1) 使成员成为“public”(公共的)。这样所有人从任何地方都可以访问它。
(2) 变成一个“友好”成员,方法是舍弃所有访问指示符,并将其类置于相同的包内。
(3) 引入“继承”概念后,一个继承的类既可以访问一个 protected 成员,也可以访问一个 public成员(但不可访问 private成员)。
(4) 提供“访问器/变化器”方法(亦称为“获取/设置”方法),以便读取和修改值。这是OOP环境中最正规的一种方法,也是 Java Beans的基础——具体情况会在第13 章介绍。
使用public关键字意味着紧随在后面的成员声明适用于所有人。假定定义了名为 dessert的包,其中包含下述单元
System.out.println("Cookie constructor");
void foo() { System.out.println("foo"); } //非dessert包不能访问
Cookie.java 必须在dessert的一个子目录内,而这个子目录又必须位于由 CLASSPATH 指定的C05目录下面(代表第 5章)。不要以为 Java 都会将当前目录作为搜索的起点看待。如果不将一个“.”作为 CLASSPATH 的一部分使用,Java 就不会考虑当前目录。
System.out.println("Dinner constructor");
public static void main(String[] args) {
大家可能会发现下面这些代码得以顺利编译——尽管它看起来违背了规则:
public static void main(String[] args) {
void f() { System.out.println("Pie.f()"); }
通常会认为Pie和f()是“友好的”,但不适用于Cake。由于它们位于相同的目录中,而且没有明确的包名。Java 把象这样的文件看作那个目录“默认包”的一部分,所以它们对于目录内的其他文件来说是“友好”的。
private关键字意味着除非从特定的类的方法里,否则没有人能访问那个成员。同一个包内的其他成员不能访问 private成员。所以private 允许我们自由地改变,毋需关心它是否会影响同一个包内的另一个类。开始的时候通常会认为自己不必使用private关键字,因为完全可以在不用它的前提下发布代码(与C++鲜明对比)。然而,随着学习大家就会发现private 仍然有非常重要的用途,特别是在涉及多线程处理的时候(详情见第14 章)。
public static void main(String[] args) {
Sundae x = Sundae.makeASundae();
有时可能想控制对象的创建方式,并防止有人直接访问一个特定的构建器(或者所有构建器)。在上面的例子中,不可通过它的构建器创建一个Sundae 对象;相反,必须调用makeASundae()方法来实现(注释③)。
③:由于默认构建器是唯一获得定义的,而且它的属性是 private,所以可防止对这个类的继承(第6 章要重点讲述的主题)。
protected 引入了一种名为“继承”的概念,它以现有的类为基础,并在其中加入新的成员,同时不会对现有的类(“基础类”(Base Class))产生影响。亦可改变那个类现有成员的行为。对于继承,我们说新类“扩展”(extends)了那个现有的类。如下所示:
若从另一个包内的某个类里继承,则唯一能够访问就是另一个包的public 成员。如果在相同的包里进行继承,那么继承获得的包能够访问所有“友好”的成员。有些时候,基础类的创建者喜欢提供一个特殊的成员,并允许访问衍生类。这正是protected 的工作。若往回引用5.2.2 小节“public:接口访问”的那个Cookie.java 文件,则下面这个类就不能访问“友好”的成员:
public class ChocolateChip extends Cookie {
System.out.println("ChocolateChip constructor");
public static void main(String[] args) {
ChocolateChip x = new ChocolateChip();
//! x.foo(); // Can't access foo
值得注意的是 foo()也会存在于从Cookie继承的类中。但由于foo()在外部的包里是“友好”的,所以我们不能使用它。当然,可将其变成public。但这样一来,所有人都能自由访问它。若象下面这样修改类Cookie:
System.out.println("Cookie constructor");
那么仍然能在包dessert里“友好”地访问 foo(),而且从Cookie 继承的其他类亦可自由地访问它,即使它并非公共的(public)。
为清楚起见,可考虑用特殊的样式创建一个类:将 public成员置于最开头,后面跟随protected、友好以及private成员。这样做的好处是类的使用者可从上向下依次阅读,并首先看到对自己来说最重要的内容(即public成员,因为它们可从文件的外部访问),并在遇到非公共成员后停止阅读。利用由javadoc 提供支持的注释文档(第2 章介绍),代码可读性已很大程度上得到了解决。
若想一个类能由客户程序员调用,可在类主体的起始花括号前面某处放置一个 public关键字。所以我们能够使用:
也就是说,假若我们的库名是mylib,那么所有客户程序员都能访问 Widget——通过下述语句:
(1) 每个编译单元(文件)都只能有一个public 类。根据自己的需要,它可拥有任意多个提供支撑的“友好”类。但若在一个编译单元里使用了多个public类,编译器就会报错。
(2) public类的名字必须与包含了编译单元的那个文件的名字完全相符,甚至包括它的大小写形式。所以对于Widget 来说,文件的名字必须是Widget.java
(3) 可能(但并常见)有一个编译单元根本没有任何公共类。此时,可按自己的意愿任意指定文件名。
注意不可将类设成private(那样会使除类之外的其他东西都不能访问它),也不能设成 protected(注释④)。因此,我们现在对于类的访问只有两个选择:“友好的”或者public。若不愿其他任何人访问那个类,可将所有构建器设为private。这样一来,在类的一个static 成员内部,除自己之外的其他所有人都无法创建属于那个类的一个对象(注释⑤)。如下例所示:
④:实际上,Java 1.1 内部类既可以是“受到保护的”,也可以是“私有的”,但那属于特别情况。第 7 章会详细解释这个问题。
private Soup() {} // (1) Allow creation via static method:
public static Soup makeSoup() {
// (2) Create a static object and return a reference upon request.
private static Soup ps1 = new Soup(); //“独子”方案:
class Sandwich { // Uses Lunch
// Only one public class allowed per file:
// Can't do this! Private constructor:
迄今为止,我们创建过的大多数方法都是要么返回 void,要么返回一个基本数据类型。所以对下述定义来说:
它会多少会使人有些迷惑。void 在英语里是“虚无”的意思。但亦可返回指向一个对象的句柄,此时就是这个情况。该方法返回一个句柄,它指向类Soup 的一个对象。
Soup 类通过将所有构建器都设为 private,从而防止直接创建一个类,就没人能为那个类创建一个对象。但怎样使用这个类呢?
第一个选择,可创建一个static 方法,再通过它创建一个新的Soup,然后返回指向它的一个句柄。如果想在返回之前对Soup 进行一些额外的操作,或者想了解准备创建多少个 Soup 对象,这种方案无疑是特别有用的。
第二个选择是采用“设计方案”(Design Pattern)技术,通常叫作“独子”,因为它仅允许创建成有且只有一个 static private 成员。除非通过public 方法access(),否则根本无法访问它。
正如早先指出的那样,类的对象可由包内的其他类创建,但不能由包外创建。请记住,对于相同目录内的所有文件,如果没有明确地进行package 声明,那么它们都默认为那个目录的默认包的一部分。然而,假若那个类一个static成员的属性是public,那么客户程序员仍然能够访问那个static 成员——即使它们不能创建属于那个类的一个对象。