二十二、封装和方法
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 修饰类成员
private
和 public
是两个非常常用的访问控制修饰符。
用 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 小结
方法可以让外部代码安全地访问实例字段;
方法是一组执行语句,并且可以执行任意逻辑;
外部代码通过public方法操作实例,内部代码可以调用private方法;