本章详细介绍了对象封装的原因及过程,几种常见的程序代码块及单例模式的编写。
7.1 为什么要封装
范例1:EncapsulationDemo01.java
class Person{ String name; int age; void say(){ System.out.println("我叫"+name+",今年"+age+"岁。"); } } public class EncapsulationDemo01{ public static void main(String[] args){ Person person = new Person(); person.name = "张三"; person.age = 20; person.say(); } }
本例非常简单,程序的输出结果是:我叫张三,今年20岁。
分析:本例中定义了两个类,一个是Person类,一个是EncapsulationDemo01类,在EncapsulationDemo01类中首先实例化了Person类,然后通过Person类的实例化对象进行赋值操作。看上去貌似没有什么问题,但真是这样么?看下范例2。
范例2:EncapsulationDemo02.java
class Person{ String name; int age; void say(){ System.out.println("我叫"+name+",今年"+age+"岁。"); } } public class EncapsulationDemo02{ public static void main(String[] args){ Person person = new Person(); person.name = "张三"; person.age = -20; person.say(); } }
本例代码运行时没有任何问题,但是由于不小心把age的值指定为了-20,导致结果发生了逻辑错误,因为人(Person)的年龄(age)不可能是负数。由此可见,通过Person实例对象来给age属性赋值的方式是不安全的。并且age本身是Person类的属性,外界根本不应该拥有直接操纵该属性的权利。而上面两个范例中Person类中的属性都完全的暴露给了使用它的对象。为了程序的安全性和健壮性,所以需要封装对象。
7.2 什么是封装
所谓封装就是把对象的属性私有化,不让外部对象直接访问。在程序开发中,通常使用private关键词来实现该功能。语法格式为:private 类型 属性名(或方法名)。被private声明的属性或方法只能在该类使用,而不能在为的外部使用。封装的目的是为了提高程序的安全性。
7.3 如何封装
范例3:EncapsulationDemo03.java
class Person{ private String name; private int age; void say(){ System.out.println("我叫"+name+",今年"+age+"岁。"); } } public class EncapsulationDemo03{ public static void main(String[] args){ Person person = new Person(); person.name = "张三"; person.age = -20; person.say(); } }
本例中除了使用private关键词来修饰了name和age属性外,程序代码其它部分没有任务改动。编译程序时,出现了如下错误信息:
E:\java>javac EncapsulationDemo03.java
EncapsulationDemo03.java:11: 错误: name可以在Person中访问private
person.name = "张三";
^
EncapsulationDemo03.java:12: 错误: age可以在Person中访问private
person.age = -20;
^
2 个错误
这个错误信息说明了被private修饰后的属性不能再通过外部类来直接访问了。当外部类中不能访问Person对象中的属性后,程序就没有办法给Person对象赋值了,为了让程序既不能直接访问Person对象的属性,又可以给Person对象赋值,那又如何才能实现呢?为了解决这个问题,一般程序设计时,会提供属性对应的set和get方法。
范例4:EncapsulationDemo04.java
class Person{ private String name; private int age; public void setName(String name){ this.name = name; } public String getName(){ return name; } public void setAge(int age){ this.age = age; } public int getAge(){ return age; } void say(){ System.out.println("我叫"+name+",今年"+age+"岁。"); } } public class EncapsulationDemo04{ public static void main(String[] args){ Person person = new Person(); person.setName("张三"); person.setAge(20); person.say(); } }
本例能正常编译,且运行结果为:我叫张三,今年20岁。
分析:首先在Person类中增加了void setName(String name)、String getName()、void setAge(int age)、int getAge()这四个方法。其中setName(String name)方法是给Person类中的name属性赋值,getName()方法是获取name属性的值;setAge(int age)方法是给age属性赋值,getAge()方法是获取age属性的值。然后在EncapsulationDemo04类中实例化Person类后,通过person.setName("张三");和person.setAge(20);两行代码来初始化person对象。最后调用say()方法输出。
在Person类中加了setName()和setAge()方法后,确实可以给name和age属性赋值了,但是否安全呢?
范例5:EncapsulationDemo05.java
class Person{ private String name; private int age; public void setName(String name){ this.name = name; } public String getName(){ return name; } public void setAge(int age){ this.age = age; } public int getAge(){ return age; } void say(){ System.out.println("我叫"+name+",今年"+age+"岁。"); } } public class EncapsulationDemo05{ public static void main(String[] args){ Person person = new Person(); person.setName("张三"); person.setAge(-20); person.say(); } }
本例再次不小心把age属性的值设置为了-20,貌似好象转了一圈又回到了范例2的问题上了。那么,既然又回到了以前的问题,之前的操作是否有必要呢?
肯定是有必要的,在范例2中是不能控制数据设置的,但范例5中确可以。下面就是在setName()或setAge()方法中加入数据校验,以确保数据的有效性。
范例6:EncapsulationDemo06.java
class Person{ private String name; private int age; public void setName(String name){ if(name == null || "".equals(name)){ System.out.println("姓名需要提供"); this.name = ""; }else{ this.name = name; } } public String getName(){ return name; } public void setAge(int age){ if(age <= 0){ System.out.println("年龄不对"); this.age = 0; }else{ this.age = age; } } public int getAge(){ return age; } void say(){ if(!"".equals(name) && age > 0) System.out.println("我叫"+name+",今年"+age+"岁。"); } } public class EncapsulationDemo06{ public static void main(String[] args){ Person person = new Person(); person.setName("张三"); person.setAge(-20); person.say(); } }
本例输出结果为:年龄不对
分析:在程序第5~10行加入了对String类型数据的校验。name == null是对空对象的比较,如果name没有指定值,那么name就使用字符串对象的默认值null,比较返回值为true;如果指定了值那么比较后的返回值为false。程序第16~21行加入了对age的校验,当age的值小于或等于0时,给出提示信息,并给age初值为0。程序27行加入了输出校验。只有当name属性不为空字符串并且age属性的值大于0时才输出其信息。
小结:对象封装共有三个步骤:通过private关键词私有化属性;提供公有方法来访问私有属性;在公有方法中加入校验。
7.4 初始化的几种方式
在Java中,可以使用构造方法来初始化对象,也可以使用静态代码块来初始化对象,还可以使用普通代码块来初始化对象。那么它们的之间的执行顺序又是怎样的呢?
范例7:EncapsulationDemo07.java
class Person{ //构造方法 public Person(){ System.out.println("构造方法代码块被执行..."); } //静态代码块 static{ System.out.println("静态代码块被执行..."); } //普通代码块 { System.out.println("普通代码块被执行..."); } } public class EncapsulationDemo07{ public static void main(String[] args){ Person person = new Person(); } }
程序输出结果为:
静态代码块被执行...
普通代码块被执行...
构造方法代码块被执行...
为什么是这样一个执行顺序呢?
通过上一章的学习,我们知道被static修改的属性或方法是随着类加载而加载,随着类消失而消失的。因此,静态代码块最先被执行。而构造方法是在实例化对象时才会被执行,所以构造方法是最后执行。
7.5 构造方法私有化
程序代码中构造方法并不都是public的,有时需求不想让外部直接new 该对象,这时就需要把构造方法私有化。
范例8:PrivateConstructor.java
class Calcul{ private static Calcul instance = new Calcul(); private Calcul(){ System.out.println("私有构造方法..."); } public static Calcul getInstance(){ return instance; } } public class PrivateConstructor{ public static void main(String[] args){ Calcul.getInstance(); } }
本例就使用了构造方法私有化,当构造方法私有化后,在这个对象之外就不能实例化它了,为了能实例化这个对象,必须在这个对象中提供一个公共的方法,让这个公共的方法来调用私有构造方法,从而达到实例化对象的目的。
本例其实就是单例模式的一种编写方式。单例模式的另一种方式我们放到多线程里再讲解。
所谓单例模式,它是设计模式中的一种,单例模式的好处就在于无论创建多少个实例对象,内存只会分配一块区域。换句话说,使用单例模式创建的对象都是同一个对象。
范例9:SingletonDemo.java
class Singleton{ private static Singleton instance = new Singleton(); private Singleton(){ } public static Singleton getInstance(){ return instance; } } public class SingletonDemo{ public static void main(String[] args){ Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); Singleton s3 = Singleton.getInstance(); System.out.println(s1 == s2); System.out.println(s2 == s3); } }
程序输出结果为:
true
true
从结果可以直观的看到,创建的三个Singleton对象都是指向同一个地址,因此是同一个对象。