二十二、封装和方法

1 初识封装

面向对象有三个基本特征——封装、继承、多态。

继承和多态在后面的章节会详细介绍,这里给读者简要介绍一下封装。

封装的目的是简化编程和增强安全性。

(1)简化编程是指封装可以让使用者不必了解具体类的内部实现细节,而只要通过提供给外部访问的方法来访问类中的属性和方法。例如Java API中的 Arrays.sort() 方法,该方法可以用于给数组进行排序操作,开发者只需要将待排序的数组名放到 Arrays.sort() 方法的参数中,该方法就会自动地将数组排好序。可见,开发者根本不需要了解 Arrays.sort() 方法的底层逻辑,只需要简单地将数组名传递给方法即可实现排序。

(2)增强安全性是指封装可以使某个属性只能被当前类使用,从而避免这个属性被其他类或对象进行误操作。例如在下面的程序中,我们可以轻松地将一个人的年龄属性设置为负数。

public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.name = "Xiao Ming"; // 对字段name赋值
        ming.age = -12; // 对字段age赋值
        System.out.println(ming.name); // 访问字段name
        System.out.println(ming.age); // 访问字段age,输出-12
    }
}
class Person {
    public String name;    // 姓名
    public int age;    // 年龄
}

在这段程序中,给 age 赋了一个不符合逻辑的值,但语法却是正确的。因此,这种做法实际就给程序造成了安全问题。如何避免此类问题呢?

要解决这个问题,我们需要了解一些访问控制修饰符相关的知识。

2 private 和 public 修饰类成员

privatepublic 是两个非常常用的访问控制修饰符。

private 修饰的类成员,只能被该类自身的方法访问和修改,而不能被任何其他类访问和引用。

public 修饰的类成员,可以被任何可以访问此类的类访问和修改。

3 getter() 和 setter()

因此,我们可以使用 private 修饰符来修饰 age 属性,以此禁止 Person 以外的类对 age 属性的进行访问和修改。

public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.name = "Xiao Ming"; // 报错
        ming.age = -12; // 报错
    }
}
class Person {
    private String name;    // 姓名
    private int age;    // 年龄
}

但是,类中的属性如果不能被访问和设置,安全倒是非常安全,但这个类也就变得无法使用了。

有没有一种办法,既能让其他类可以访问 Person 类中的 age 属性,又能保证其他类始终是在安全的数值范围内修改 age 值呢?答案是有,先用 private 修饰 age 属性,然后再给该属性提供两个 public 修饰的、保证属性安全的访问方法(setter() 方法和getter()方法)。

getter() 用于获取属性的值,方法名通常是get+属性名。

public int getAge() {
    return age;
}

setter() 用于给属性赋值,方法名通常是set+属性名。

public void setAge(int age) {
    if (age < 0 || age > 100){
        System.out.println("请输入正确的年龄值!");
    }else {
        this.age = age;
    }
}

这里我们在 setAge() 方法中,对传入参数进行了检查,如果参数超出年龄正常范围,将无法成功赋值。

4 this.属性名

this 是一个变量,是在每个类中隐含的变量,它始终指向当前实例。

大部分时候,普通方法访问其他方法、成员变量时无须使用 this 前缀,但如果方法里有个局部变量和成员变量同名,但程序又需要在该方法里访问这个被覆盖的成员变量,则必须使用 this 前缀。

例如上面的 setAge() 方法中,我们为传入 setAge() 方法的参数取名为 age ,而 Person 类中,年龄字段的名字也是 age ,我们需要使用 this 关键字告诉计算机, this.age 中的 age ,是 Person 的字段,而不是传入的参数。

5 private 方法

public方法,自然就有private方法。和private字段一样,private方法不允许外部调用,那我们定义private方法有什么用?

定义private方法的理由是内部方法是可以调用private方法的。例如:

public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.setBirth(2008);
        System.out.println(ming.getAge());
    }
}

class Person {
    private String name;
    private int birth;

    public void setBirth(int birth) {
        this.birth = birth;
    }

    public int getAge() {
        return calcAge(2019); // 调用private方法
    }

    // private方法:
    private int calcAge(int currentYear) {
        return currentYear - this.birth;
    }
}

观察上述代码,calcAge()是一个private方法,外部代码无法调用,但是,内部方法getAge()可以调用它。

此外,我们还注意到,这个Person类只定义了birth字段,没有定义age字段,获取age时,通过方法getAge()返回的是一个实时计算的值,并非存储在某个字段的值。这说明方法可以封装一个类的对外接口,调用方不需要知道也不关心Person实例在内部到底有没有age字段。

6 可变参数

可变参数用类型...定义,可变参数相当于数组类型:

class Group {
    private String[] names;

    public void setNames(String... names) {
        this.names = names;
    }
}

上面的setNames()就定义了一个可变参数。调用时,可以这么写:

Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames("Xiao Ming"); // 传入1个String
g.setNames(); // 传入0个String

完全可以把可变参数改写为String[]类型:

class Group {
    private String[] names;

    public void setNames(String[] names) {
        this.names = names;
    }
}

但是,调用方需要自己先构造String[],比较麻烦。例如:

Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]

另一个问题是,调用方可以传入null

Group g = new Group();
g.setNames(null);

而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null

特别注意:因为在调用方法时,传入的实参必须严格按照形参的定义顺序一一传递,而可变参数的个数不确定,所以,一个方法中,可变参数最多只能有一个,而且只能是最后一个参数。

例如,下面的例子,计算机将无法确定,我们想要传入的字符串应该如何分配给两个形参。

public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.setNames("小明", "小蓝", "小红");
    }
}
class Person {
    private String name;    // 姓名
    private String[] myFriends;    // 年龄

    public void setNames(String... friendNames, String name) {
        this.name = name;
        this.myFriends = friendNames;
    }
}

我们只能这样写:

public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.setNames("小明", "小蓝", "小红");
    }
}

class Person {
    private String name;    // 姓名
    private String[] myFriends;    // 年龄

    public void setNames(String name, String... friendNames) {
        this.name = name;
        this.myFriends = friendNames;
    }
}

这样,计算机才知道将 "小明" 分配给 name,其他字符串传入 friendNames

7 小结

  1. 方法可以让外部代码安全地访问实例字段;

  2. 方法是一组执行语句,并且可以执行任意逻辑;

  3. 外部代码通过public方法操作实例,内部代码可以调用private方法;