理解为什么要进行封装?封装有什么好处?封装的代码怎么实现?
封装是面向对象的三大特征之一,什么是封装?封装有什么好处?怎么封装,代码怎么写?这是大家这一章节要学习的内容。
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节,但可以通过该对象对外提供的接口来访问该对象。
在现实世界当中我们可以看到很多事物都是封装好的,比如“鼠标”,外部有一个壳,将内部的原件封装起来,至于鼠标内部的细节是什么,我们不需要关心,只需要知道鼠标对外提供了左键、右键、滚动滑轮这三个简单的操作。对于用户来说只要知道左键、右键、滚动滑轮都能完成什么功能就行了。为什么鼠标内部的原件要在外部包装一个“壳”呢,起码内部的原件是安全的,不是吗。再如“数码相机”,外部也有一个壳,将内部复杂的结构包装起来,对外提供简单的按键,这样每个人都可以很快的学会照相了,因为它的按键很简单,另外照相机内部精密的原件也受到了壳儿的保护,不容易坏掉。
根据以上的描述,可以得出封装有什么好处呢?封装之后就形成了独立实体,独立实体可以在不同的环境中重复使用,显然封装可以降低程序的耦合度,提高程序的扩展性,以及重用性或复用性,例如“鼠标”可以在A电脑上使用,也可以在B电脑上使用。另外封装可以隐藏内部实现细节,站在对象外部是看不到内部复杂结构的,对外只提供了简单的安全的操作入口,所以封装之后,实体更安全了。
我们来看一段代码,在不进行封装的前提下,存在什么问题:
public class MobilePhone {
//电压:手机正常电压在3~5V
double voltage;
}
public class MobilePhoneTest {
public static void main(String[] args) {
MobilePhone phone = new MobilePhone();
phone.voltage = 3.7;
System.out.println("手机电压 = " + phone.voltage);
phone.voltage = 100;
System.out.println("手机电压 = " + phone.voltage);
}
}
运行结果如下图所示:
图10-1:未进行封装的程序测试
以上程序MobilePhone类未进行封装,其中的电压属性voltage对外暴露,在外部程序当中可以对MobilePhone对象的电压voltage属性进行随意访问,导致了它的不安全,例如手机的正常电压是3~5V,但是以上程序已经将手机电压设置为100V,这个时候显然是要出问题的,但这个程序编译以及运行仍然是正常的,没有出现任何问题,这是不对的。
为了保证内部数据的安全,这个时候就需要进行封装了,封装的第一步就是将应该隐藏的数据隐藏起来,起码在外部是无法随意访问这些数据的,怎么隐藏呢?我们可以使用java语言中的private修饰符,private修饰的数据表示私有的,私有的数据只能在本类当中访问。请看程序:
public class MobilePhone {
//电压:手机正常电压在3~5V
private double voltage;
}
public class MobilePhoneTest {
public static void main(String[] args) {
MobilePhone phone = new MobilePhone();
phone.voltage = 3.7;
System.out.println("手机电压 = " + phone.voltage);
phone.voltage = 100;
System.out.println("手机电压 = " + phone.voltage);
}
}
以上程序编译报错了,请看下图:
图10-2:private修饰的数据无法在外部程序中直接访问
通过以上的测试,手机对象的电压属性确实受到了保护,在外部程序中无法访问了。但从当前情况来看,voltage属性有点儿太安全了,一个对象的属性无法被外部程序访问,自然这个数据就没有存在的价值了。所以这个时候就需要进入封装的第二步了:对外提供公开的访问入口,让外部程序统一通过这个入口去访问数据,我们可以在这个入口处设立关卡,进行安全控制,这样对象内部的数据就安全了。
对于“一个”属性来说,我们对外应该提供几个访问入口呢?通常情况下我们访问对象的某个属性,不外乎读取(get)和修改(set),所以对外提供的访问入口应该有两个,这两个方法通常被称为set方法和get方法(请注意:set和get方法访问的都是某个具体对象的属性,不同的对象调用get方法获取的属性值不同,所以set和get方法必须有对象的存在才能调用,这样的方法定义的时候不能使用static关键字修饰,被称为实例方法。实例方法必须使用“引用”的方式调用。还记得之前我们接触的方法都是被static修饰的,这些方法直接采用“类名”的方式调用,而不需要创建对象,在这里显然是不行的)。请看以下代码:
public class MobilePhone {
//电压:手机正常电压在3~5V
private double voltage;
public MobilePhone(){
}
public void setVoltage(double _voltage){
if(_voltage < 3 || _voltage > 5){
//当电压低于3V或者高于5V时抛出异常,程序则终止
throw new RuntimeException("电压非法,请爱护手机!");
}
//程序如果能执行到此处说明以上并没有发生异常,电压值合法
voltage = _voltage;
}
public double getVoltage(){
return voltage;
}
}
public class MobilePhoneTest {
public static void main(String[] args) {
MobilePhone phone = new MobilePhone();
phone.setVoltage(3.7);
System.out.println("手机电压 :" + phone.getVoltage());
phone.setVoltage(100);
System.out.println("手机电压 :" + phone.getVoltage());
}
}
运行结果如下图所示:
图10-3:对封装之后的测试
通过以上程序,可以看出MobilePhone的voltage属性不能在外部程序中随意访问了,只能调用MobilePhone的setVoltage()方法来修改电压,调用getVoltage()方法来读取电压,在setVoltage()方法中编写了安全控制代码,当电压低于3V,或者高于5V的时候,程序抛出了异常,不允许修改电压值,程序结束了。只有合法的时候,才允许程序修改电压值。(异常机制在后续的内容中会学到,不要着急。)
总之,在java语言中封装的步骤应该是这样的:需要被保护的属性使用private进行修饰,给这个私有的属性对外提供公开的set和get方法,其中set方法用来修改属性的值,get方法用来读取属性的值。并且set和get方法在命名上也是有规范的,规范中要求set方法名是set + 属性名(属性名首字母大写),get方法名是get + 属性名(属性名首字母大写)。其中set方法有一个参数,用来给属性赋值,set方法没有返回值,一般在set方法内部编写安全控制程序,因为毕竟set方法是修改内部数据的,而get方法不需要参数,返回值类型是该属性所属类型(先记住,以后讲:另外set方法和get方法都不带static关键字,不带static关键字的方法称为实例方法,这些方法调用的时候需要先创建对象,然后通过“引用”去调用这些方法,实例方法不能直接采用“类名”的方式调用。),例如以下代码:
public class Product {
private int no;
private String name;
private double price;
public Product(){
}
public Product(int _no , String _name , double _price){
no = _no;
name = _name;
price = _price;
}
public int getNo() {
return no;
}
public void setNo(int _no) {
no = _no;
}
public String getName() {
return name;
}
public void setName(String _name) {
name = _name;
}
public double getPrice() {
return price;
}
public void setPrice(double _price) {
price = _price;
}
}
public class ProductTest {
public static void main(String[] args) {
Product p1 = new Product(10000 , "小米5S" , 2000.0);
System.out.println("商品编号:" + p1.getNo());
System.out.println("商品名称:" + p1.getName());
System.out.println("商品单价:" + p1.getPrice());
p1.setNo(70000);
p1.setName("小米6");
p1.setPrice(2100.0);
System.out.println("商品编号:" + p1.getNo());
System.out.println("商品名称:" + p1.getName());
System.out.println("商品单价:" + p1.getPrice());
}
}
运行结果如下图所示:
图10-4:set和get方法测试
有的读者可能会有这样的疑问:构造方法中已经给属性赋值了,为什么还要提供set方法呢?注意了同学们,这是两个完全不同的时刻,构造方法中给属性赋值是在创建对象的时候完成的,当对象创建完毕之后,属性可能还是会被修改的,后期要想修改属性的值,这个时候就必须调用set方法了。