2 类与方法
不同于 C ,Java 是一门面向对象的编程语言。C++ 也有面向对象的内容,但是 C++ 和 Java 在方法的具体实现上存在区别。
方法的定义
方法(method)是为执行一个复杂操作组合在一起的语句集合。一个类中可以声明多个方法。其语法是采用 BNF 范式(Backus-Naur Form,巴科斯范式)描述的,用来描述计算机语言语法的符号集。
例如,下面是一个求两个整数中最大值的方法max
:
| public static int max(int num1,int num2){
int result = 0;
if(num1 > num2)
result = num1;
else
result = num2;
return result;
}
|
其中,public
和static
是修饰符,int
是返回值,max
是方法名称。
方法签名(Method Signature)是指方法名称+形参列表,如上面方法的签名就是max(int num1,int num2)
。一个类中不能包含方法签名相同的多个方法,因为这样在调用方法时编译器不知道要调用哪个。
方法的调用
Java 类中的成员方法可以分为构造方法、类方法和对象方法。
构造方法的调用
构造方法只能在新建一个对象时,由 Java 虚拟机进行调用。创建对象通常是通过类名 对象名 = new 类名(构造函数的参数)
建立。比如下面的例子:
| class TestConstructor{
private int value;
public TestConstructor(){this.value = 1;}
public TestConstructor(int value){this.value = value;}
public int getValue(){return this.value;}
}
public class CallConstructor{
public static void main(String[ ] args) {
TestConstructor tc = new TestConstructor();
System.out.println("Value: " + tc.getValue());
tc = new TestConstructor(5);
System.out.println("Value: " + tc.getValue());
}
}
|
运行的结果是:
this 调用
可以通过this
调用该类的其它构造函数,但是必须是构造函数的第一条语句。例如:
| class TestConstructor{
private int value;
public TestConstructor(){this(114);}
public TestConstructor(int value){this.value = value;}
public int getValue(){return this.value;}
}
public class ThisConstructor{
public static void main(String[ ] args) {
TestConstructor tc = new TestConstructor();
System.out.println("Value: " + tc.getValue());
}
}
|
运行的结果为Value: 114
。这是因为在调用无参数构造函数时,无参构造函数通过this(114)
调用了构造函数TestConstructor(int value)
,并给其形参传递值value = 114
,从而使得value
成员初始化为114
。
super 调用
一个类还能够通过super调用其父类的构造函数。例如
| class TestConstructor{
private int value;
public TestConstructor(){this(114);}
public TestConstructor(int value){this.value = value;}
public int getValue(){return this.value;}
}
public class SuperConstructor extends TestConstructor{
public SuperConstructor(){super(514);}
public static void main(String[ ] args) {
SuperConstructor sc = new SuperConstructor();
System.out.println("Value: " + sc.getValue());
}
}
|
运行的结果为Value: 514
。这是因为在调用SuperConstructor
类的无参构造函数时,通过super(514)
语句调用了父类TestConstructor
中的含参构造函数TestConstructor(int value)
,并给其形参传递值value = 514
,从而使成员value
初始化为514
。
类方法的调用
对象方法只能通过实例对象来调用。比如下面的代码:
| class Wallet{
private int money;
Wallet(){money = 0;}
public void addMoney(int amount){this.money += amount;}
public int getMoney(){return this.money;}
}
public class CallObjectMethod{
public static void main(String[] args){
Wallet wallet = new Wallet();
System.out.println("Now we have money: " + wallet.getMoney());
wallet.addMoney(520);
System.out.println("Then we have money: " + wallet.getMoney());
}
}
|
运行结果为:
| Now we have money: 0
Then we have money: 520
|
可以看到实例方法addMoney
和getMoney
都是通过对象名.方法名
调用的。显然不能通过Wallet.addmoney
或者Wallet.getMoney
调用这两个方法,因为我可以有多个Wallet
对象wallet1,wallet2,...
,使用类名.方法名
无法知道调用的是哪个Wallet
对象。
方法的重载
方法重载(Overloading)是指方法名称相同,但形参列表不同的方法。仅返回类型不同的方法不是合法的重载。一个类中可以包含多个重载的方法(同名的方法可以重载多个版本)。
方法重载实例
方法重载,例如
| public class InputCheckTest{
public static void CheckInput(String s){System.out.println("You entered a string.");}
public static void CheckInput(int i){System.out.println("You entered an integer.");}
public static void CheckInput(double d){System.out.println("You entered a double.");}
public static void main(String[] args){
CheckInput(114514);
CheckInput("114514");
CheckInput(114514.0);
}
}
|
运行结果:
| You entered an integer.
You entered a string.
You entered a double.
|
有歧义的重载
系统根据我们的输入自动判断调用哪个方法。但是有时候,可能会有多个合适的方法。例如
| public class AmbiguousOverloading {
public static void main(String[ ] args) {
// System.out.println(max(1, 2));
}
public static double max(int num1, double num2) {
return (num1 > num2)?num1:num2;
}
public static double max(double num1, int num2) {
return (num1 > num2)?num1:num2;
}
}
|
将注释符号去掉,上面的代码编译时将产生错误,因为编译器不知道max(1,2)
调用的是哪个函数。
包和类的导入
Java 源程序在开头通过import 包名;
语句导入其它包(类),可以使用其它包中的类及其方法。有点类似于 C/C++ 中的#include "头文件名"
语句。Java 程序在编译时会自动导入java.lang.System
类,所以我们在编写源程序时可以直接使用System.out
和System.in
以及它们的方法。
总的来说,import
有两种类型:单类型导入和按需类型导入。
单类型导入
把导入的标识符引入到当前.java文件,因此当前文件里不能定义同名的标识符,类似C++中的using nm::id
; 把名字空间nm
的名字id
引入到当前代码处。
比如,在包p1
中定义了类A
:
| package p1;
public class A{
// some statements...
}
|
那么在包p2
中:
| package p2;
import p1.A;//单类型导入,把p1.A引入到当前域
//这个时候当前文件里不能定义A,下面语句编译报错
public class A {
//some statements...
}
|
按需类型导入
是把包里的标识符都引入到当前.java文件,只是使包里名字都可见,使得我们要使用引入包里的名字时可以不用使用完全限定名,因此在当前.java文件里可以定义与引入包里同名的标识符。但二义性只有当名字被使用时才被检测到。类似于C++里的using nm
。
比如,包p1
还是和上面一样,此时p2
中:
| package p2;
import p1.*; //按需导入,没有马上把p1.A引入到当前域
//因此当前文件里可以定义A
public class A {
public static void main(String[] args){
A a1 = new A(); //这时A是p2.A
System.out.println(a1 instanceof p2.A); //true
//当前域已经定义了A,因此要想使用package p1里的A,只能用完全限定名
p1.A a2 = new p1.A();
}
}
|
如果出现了名字冲突,要用完全限定名消除冲突。
类及其方法的可见性修饰符
Java 中的可见性修饰符有private
、public
和protected
,不加可见性修饰符的方法默认访问权限为包级。Java 继承时无继承控制(即都是公有继承,和 C++ 不同),故父类成员继承到派生类时访问权限保持不变(除了私有)。
成员访问控制符的作用:
private
: 只能被当前类定义的函数访问。
- 包级:无修饰符的成员,只能被同一包中的类访问。
protected
:子类、同一包中的类的函数可以访问。
public
: 所有类的函数都可以访问。
访问权限 | 本类 | 本包 | 子类 | 它包 |
---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | No(非子类时) |
包级(默认) | Yes | Yes | No(非本包时) | No |
private | Yes | No | No | No |
其它的都很好理解,只是需要注意一点:子类类体中可以访问从父类继承来的protected
成员。但如果子类和父类不在同一个包里,子类里不能访问另外父类实例的protected
成员。可以举一个例子说明这一点。我们在p1
包里定义C1
类:
| package p1;
public class C1{
protected int u = 3;
}
|
然后在包p2
中,会出现以下情况:
| package p2;
import p1.C1;
public class C2 extends C1{
int u = 5;
public static void main(String[] args){
System.out.println(u) // OK. u 相当于 this.u,打印结果是 5。
System.out.println(super.u) // OK. super.u 指的是父类 C1 中的 u,在子类中能够访问。打印结果是 3。
C1 o = new C1();
System.out.println(o.u) // ERROR. 虽然这里是 C1 的子类,但是与 C1 不在同一个包中,且 o 是一个另外的父类实例,不能访问 o 的保护成员。
}
}
|
类的继承
Java 中可以用extends
关键字表示继承,在上面举的例子中也能看到。子类对象是一个父类对象,即子类对象实例 instanceof 父类名
的结果为true
。
方法覆盖与方法隐藏
如果子类重新定义了从父类中继承的实例方法,称为方法覆盖(Method Override)。如果子类重新定义了从父类中继承的类方法,称为方法隐藏(Method Hidden)。它们具有如下性质:
- 仅当父类方法在子类里是可访问的,该实例方法才能被子类覆盖/隐藏;否则只是定义了一个普通方法而已,不叫作覆盖/隐藏。
- 父类的
final
方法不能被子类覆盖/隐藏,否则编译时会报错。
- 在子类函数中可以使用
super
调用被覆盖的父类方法。
实例方法的多态性
之所以会产生实例方法的多态性这个概念,是因为父类引用变量可以指向子类对象。例如,如果A
是B
的父类,那么A o = new B()
是可行的。其中,A
是变量o
的声明类型,编译时对有关变量o
的语句进行检查时,都是将o
视作声明类型;B
是o
的运行时类型,在运行的时候,因为o
实际指向的对象并不是A
类型的,这就会产生多态性。
实例方法的多态性可以通过下面的代码说明:
| class A{
public void m() {
System.out.println("A's m");
}
public static void s() {
System.out.println("A's s");
}
}
class B extends A{
//覆盖父类实例方法
public void m() {
System.out.println("B's m");
}
//隐藏父类静态方法
public static void s() {
System.out.println("B's s");
}
}
public class OverrideDemo {
public static void main(String[] args) {
A o1 = new B();
o1.m(); // B's m
o1.s(); // A's s
((A)o1).m // B's m,被覆盖的父类实例方法不能再发现
B o2 = new B();
o2.s // B's s
((A)o2).s // A's s,被隐藏的父类静态方法可以再发现
}
}
|
对于上面运行结果的解释是:静态函数没有多态性,函数入口地址在编译时确定,编译时所有的变量都是按照其声明类型;实例方法具有多态性,在运行时时根据实际的运行时类型确定函数入口地址。