Java基础教程(12)--深入理解类
一.方法的返回值
当我们在程序中调用方法时,虚拟机将会跳转到对应的方法中去执行。当以下几种情况发生时,虚拟机将会回到调用方法的语句并继续向下执行:
- 执行完方法中所有的语句;
- 遇到return语句;
- 方法抛出一个异常(有关异常的内容将会在后面的文章中讨论)。
这里我们重点介绍return语句。return语句用来返回一个值,当虚拟机遇到return语句时将会立刻结束当前方法并带着返回值回到调用此方法的地方。在声明方法时,返回值的类型要和return语句里返回的值的类型一致。如果方法没有需要返回的值,可以将返回值设置为void。在返回值类型为void的方法中也可以使用return语句,格式为:
return;
它的作用仅仅是为了结束函数的执行。如果在返回值类型为void的方法中返回一个值,将会出现编译错误。
返回值的类型既可以是基本数据类型,也可以是引用类型。当返回值类型是基本数据类型时,它返回的是基本数据类型的值;当返回值类型是引用类型时,它返回的是对这个对象的引用。
当一个方法的返回值类型是一个类时,返回的值必须是对这个类或它的子类的对象的引用。当返回值类型是一个接口时,返回的值必须是对实现了这个接口的类的对象的引用。
二.this关键字
在方法或构造器中,this用来引用当前对象,可以通过this来访问当前对象的所有成员。
使用this关键字的最常见的原因是因为域会被方法中的参数或变量覆盖。此时,使用this关键字可以非常清晰的分辨它们而不是给变量或参数换一个名字。就像下面这样:
public class point { public int x = 0; public int y = 0; public point(int x, int y) { this.x = x; this.y = y; } }
this关键字还可以用来代表构造方法。下面是另一个rectangle类的实现:
public class rectangle { private int x, y; private int width, height; public rectangle() { this(0, 0, 1, 1); } public rectangle(int width, int height) { this(0, 0, width, height); } public rectangle(int x, int y, int width, int height) { this.x = x; this.y = y; this.width = width; this.height = height; } ... }
这样可以提高代码的复用性而不必为每一个构造器都编写类似的代码。在一个构造器中调用另一个构造器时,必须将这行代码放在第一行。
三.控制对类成员的访问权限
权限修饰符决定了其他类是否可以使用某个类或访问某个类的成员。有以下两种级别的权限控制。
1.类级别的访问权限
类级别的访问权限通过加在class关键字之前的权限修饰符决定,它控制的是其他类对该类的访问权限,只有public或包私有(就是没有权限修饰符)两种。public权限意味着其他所有的类都可以使用该类,而包私有权限则意味着只有在同一个包(有关包的概念将会在下一篇教程中介绍)中的类才可以使用该类。你可能在在后面的有关内部类的文章中会看到class关键字前面可以使用private或protected,但这个类实际上已经是某个类的成员,它的访问权限属于成员级别的访问权限。
2.成员级别的访问权限
成员级别的访问权限通过加在类的成员之前的权限修饰符来决定,它控制的是其他类对该成员的访问权限,分为public、protected、包私有和private。public意味着其他所有的类都可以访问该成员,protected意味着只有该类所在包中的其他类或该类的子类才可以访问该成员,包私有意味着只有该类所在包中的其他类可以访问该成员,private意味着该成员只能在该类中访问。下面的表格显示了它们的关系:
例如,下面的几个类关系如下:
下面的表格显示了这几个类对alpha类中被不同权限修饰符修饰的成员的访问权限:
一般来说应该尽可能的对域使用private,除非它是一个公共的常量,这种情况下应该使用public。
四.静态成员
1.静态域
当创建一个类的许多对象时,每个对象都有属于自己的非静态域,这些变量存储在不同的内存位置。有时,我们希望某个域被所有对象共享,或者说这个域属于整个类而不是每个对象。使用static修饰符的域被称为静态域或类变量,该类的每一个实例共享这个变量,这个变量存在于内存中的一个固定的位置。该类的任何实例都可以更改静态域的值,也可以在不创建类实例的情况下访问静态域。
例如,假设要创建多个bicycle对象并为每个对象分配一个id,第一个对象id为1。此id号对于每个对象都是唯一的,因此是一个实例变量。同时,您需要一个变量来表示bicycle已创建的对象数,以便知道要分配给下一个对象的id。这个域与任何单个对象无关,而与整个类有关,可以将这个域设置为静态域。例如:
public class bicycle { private int cadence; private int gear; private int speed; private int id; private static int numberofbicycles = 0; public bicycle(int startcadence, int startspeed, int startgear){ gear = startgear; cadence = startcadence; speed = startspeed; id = ++numberofbicycles; } }
每当创建一个bicycle类的实例时,numberofbicycles的值都会加1,然后将它的值作为id分配给这个实例。
2.静态方法
静态方法是指使用static修饰的方法。和静态域一样,它属于类,而不是某个实例。可以通过类名去调用它而不用先创建类的实例。使用下面的语法调用静态方法:
classname.methodname(args);
尽管也可以通过创建实例去访问类的静态方法,但不鼓励这么做。通常使用静态方法去访问静态域,例如,可以通过一个静态方法去访问bicycle类的静态域numberofbicycles:
public static int getnumberofbicycles() { return numberofbicycles; }
静态域、静态方法、非静态域和非静态方法之间的访问规则如下:
- 非静态方法可以访问其他非静态方法和非静态域;
- 非静态方法可以访问静态方法和静态域;
- 静态方法可以访问其他静态方法和静态域;
- 静态方法不能访问非静态方法和非静态域。
总的来说就是非静态方法可以访问所有成员,而静态方法只能访问静态成员。
3.常量
static关键字常和final关键字一起用来定义常量。final修饰符意味着域的值不能改变。例如,下面的变量定义了一个常量pi,它的值是圆周率:
static final double pi = 3.141592653589793;
被final修饰的变量一旦赋值以后就不能再修改。常量的命名规范是大写每个字母并且使用下划线(_)分隔每个单词。
如果使用fianl修饰引用类型的变量,并不是说这个变量引用的对象不能发生任何改变,而是说这个变量不能再去引用别的对象。
五.域的初始化
现在回忆一下我们在《java基础教程(5)--变量》一文中学习过的有关域的默认值的内容。在声明域时,如果不对它进行显式的初始化,编译器将赋予它一个默认值。如果是基本数据类型的域,根据它的类型,它的默认值将会是:
如果是引用类型的域,它的默认值将会是null。
除了这种默认的初始化行为,是否还有其他方式可以在使用对象前对域进行初始化呢?答案是肯定的。下面我们将分别介绍非静态域和静态域初始化行为。
1.初始化非静态域
(1)默认初始化
正如上文所说,在声明域时,如果不对它进行赋值,编译器将赋予它一个默认值。例如:
public class bag { public int capacity; }
在创建完bag类的对象以后,capacity域的值将会是0。
(2)显式初始化
也可以在声明域的同时给它提供一个初始值,就像下面这样:
public class bag { public int capacity = 10; }
(3)在构造方法中进行初始化
构造方法也可以对域进行初始化:
public class bag { public int capacity; public bag(int capacity) { this.capacity = capacity; } }
(4)非静态代码块
下面定义了一个非静态代码块对域进行初始化:
public class bag { public int capacity; { capacity = 10; } }
与非静态代码块对应的是静态代码块,马上会在下文中介绍。编译器会将初始化块拷贝到每个构造方法最前面的位置,因此,这种方法可以用来在多个构造方法*享代码块。一个类中可以定义多个代码块,它们可以出现在类体中的任何位置,它们的执行顺序与源代码中的定义顺序相同。
(5)调用方法进行初始化
下面调用了一个方法对域进行初始化:
public class bag { public int capacity; private int initializecapacity() { return 10; } }
当然实际编写代码过程中这个方法的逻辑肯定没有这么简单,这里只是为了举个例子。如果子类想要重用这个初始化方法可以将方法的访问权限设置为protected,但是同时应该使用final修饰符来禁止子类重写(有关重写的内容会在后续文章中进行介绍)这个方法(ps:java tutorial里说在实例初始化期间调用非final方法可能会导致问题,我暂时没想到会出现什么问题,如果有知道的大神还请点拨一二,下面是原文的截图)。
在了解完这几种初始化的方式以后,我们就可以在编写代码时根据它们的性质灵活选择。但是如果这几种初始化方式同时出现的话,它们的顺序又是怎么样呢?由于默认初始化和显示初始化都是在声明域的同时完成的,这两种方式只能同时出现一种;又由于使用方法对域进行初始化的方式只是一种获取值的形式,并不属于初始化时机的讨论范围之内,它在显式初始化、构造方法和代码块中都可以使用,所以实际上可以将这五种方式总结为三种。它们的顺序如下(大于号代表左边的顺序早于右边的):
默认初始化、显示初始化>非静态代码块>构造方法
2.初始化静态域
与非静态域相比,由于静态域的初始化与类是否实例化无关,所以不讨论在构造方法中对静态域赋值的方式(不代表不可以这么做,但是不建议将静态域与类的实例之间建立任何联系)。其余四种形式都有与之对应的初始化行为,默认初始化与显式初始化较为简单,这里不再赘述。
与非静态域的非静态代码块对应的是静态代码块,一个类可以有任意数量的静态代码块,它们可以出现在类中的任何位置,编译器保证按照它们在源代码中出现的顺序调用静态代码块。
也可以使用静态方法对静态域赋值,就像非静态域那样。
静态域的初始化顺序如下:
默认初始化、显示初始化>静态代码块