跳转至

2 类与方法

  不同于 C ,Java 是一门面向对象的编程语言。C++ 也有面向对象的内容,但是 C++ 和 Java 在方法的具体实现上存在区别。

方法的定义

  方法(method)是为执行一个复杂操作组合在一起的语句集合。一个类中可以声明多个方法。其语法是采用 BNF 范式(Backus-Naur Form,巴科斯范式)描述的,用来描述计算机语言语法的符号集。
  例如,下面是一个求两个整数中最大值的方法max

1
2
3
4
5
6
7
8
public static int max(int num1,int num2){
    int result = 0;
    if(num1 > num2)
        result = num1;
    else
        result = num2;
    return result;
}
  其中,publicstatic是修饰符,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());
    }
}
  运行的结果是:
Value: 1
Value: 5

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
  可以看到实例方法addMoneygetMoney都是通过对象名.方法名调用的。显然不能通过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);
    }
}
  运行结果:
1
2
3
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.outSystem.in以及它们的方法。
  总的来说,import有两种类型:单类型导入按需类型导入

单类型导入

  把导入的标识符引入到当前.java文件,因此当前文件里不能定义同名的标识符,类似C++中的using nm::id; 把名字空间nm的名字id引入到当前代码处。
  比如,在包p1中定义了类A

1
2
3
4
package p1;
public class A{
    // some statements...
}
  那么在包p2中:
1
2
3
4
5
6
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 中的可见性修饰符有privatepublicprotected,不加可见性修饰符的方法默认访问权限为包级。Java 继承时无继承控制(即都是公有继承,和 C++ 不同),故父类成员继承到派生类时访问权限保持不变(除了私有)。
  成员访问控制符的作用:

  • private: 只能被当前类定义的函数访问。
  • 包级:无修饰符的成员,只能被同一包中的类访问。
  • protected:子类、同一包中的类的函数可以访问。
  • public: 所有类的函数都可以访问。
访问权限本类本包子类它包
publicYesYesYesYes
protectedYesYesYesNo(非子类时)
包级(默认)YesYesNo(非本包时)No
privateYesNoNoNo

  其它的都很好理解,只是需要注意一点:子类类体中可以访问从父类继承来的protected成员。但如果子类和父类不在同一个包里,子类里不能访问另外父类实例protected成员。可以举一个例子说明这一点。我们在p1包里定义C1类:

1
2
3
4
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调用被覆盖的父类方法。

实例方法的多态性

  之所以会产生实例方法的多态性这个概念,是因为父类引用变量可以指向子类对象。例如,如果AB的父类,那么A o = new B()是可行的。其中,A是变量o的声明类型,编译时对有关变量o的语句进行检查时,都是将o视作声明类型;Bo的运行时类型,在运行的时候,因为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,被隐藏的父类静态方法可以再发现
    }
}
  对于上面运行结果的解释是:静态函数没有多态性,函数入口地址在编译时确定,编译时所有的变量都是按照其声明类型;实例方法具有多态性,在运行时时根据实际的运行时类型确定函数入口地址。