JAVA程序设计3——包装类、常量池技术与单例类的实现
1 基本数据类型的包装类
1.1 包装类的对象的创建与数值属性解析
Java是面向对象的编程语言,但也提供了8种基本的数据类型(整型4种,浮点型2种,布尔型1种,字符型1种),这8种基本数据类型是不支持面向对象的程序设计机制的,只是为了照顾结构化程序设计人员的传统习惯。
Java提供的与8种基本数据类型对应的是8个包装类。
把基本类型数据包装成包装类是通过对应的包装类的构造器来实现的。不仅如此,8个包装类,除了Character这个类外,都可以在构造器里面传入一个对应基本数据类型的字符串作为参数进行实例化。Character这个类传入的是一个字符,不是字符串,而且是先创建对象再传入,而不是创建对象和传入一体化。
例子:
package chapter1; public class TestWrapper { public static void main(String[] args){ int a = 3; //将基本数据类型进行包装 Integer wrapperInt = new Integer(a); //以基本数据类型(本例是int)对应的字符串进行包装 Integer wrapperInt2 = new Integer("3"); boolean bl = true; Boolean wB = new Boolean(bl); //以基本数据类型对应的字符串进行包装 Boolean wB2 = new Boolean("true"); //下面不可以包装,因为对应的字符串不可以转换成具体的数值,但编译不会出问题 //运行时候会报NumberFormatException异常 String st = "Hello"; Long l = new Long(st); } }
特别注意:
除了Boolean类外,其他的类,传入什么,就包装成对应的对象,而对于Boolean类来说,传入的是不分大小写的true(TRUE,True等等),包装的结果都是true对象,其他情况下,除了不分大小写的true之外的任何字符串,包装的结果都是false(包括false本身)
package chapter1; public class TestWrapper { public static void main(String[] args){ int a = 3; //将基本数据类型进行包装 Integer wrapperInt = new Integer(a); //以基本数据类型(本例是int)对应的字符串进行包装 Integer wrapperInt2 = new Integer("3"); boolean bl = true; Boolean wB = new Boolean(bl); //以基本数据类型对应的字符串进行包装 Boolean wB2 = new Boolean("true"); System.out.println(new Boolean("hello")); Character c = 'c'; System.out.println(new Character(c)); //下面不可以包装,因为对应的字符串不可以转换成具体的数值,但编译不会出问题 // String st = "Hello"; // Long l = new Long(st); } }
如果希望解析得到包装类对象中包装的基本类型变量,则可以使用包装类提供的xxxValue()实例方法,也就是对应的函数是获得一个基本数据类型对象的数值属性
package chapter1; public class TestWrapper { public static void main(String[] args){ int a = 3; //将基本数据类型进行包装 Integer wrapperInt = new Integer(a); System.out.println(wrapperInt.intValue()); boolean bl = true; Boolean wB = new Boolean(bl); System.out.println(wB.booleanValue()); } } 输出结果: 3 true
1.2 自动装箱和自动拆箱
上面由基本数据类型创建包装类对象以及由包装类对象解析对应的基本数据类型数值是很麻烦的,为此Java提供了自动装箱(基本数据类型自动包装成包装类)和自动拆箱(包装类自动解析出对象的数值属性)
package chapter1; public class AutoBoxingUnboxing { public static void main(String[] args){ //自动装箱 Integer a = 3; System.out.println(a); Boolean b = true; System.out.println(b); Character c = 'c'; System.out.println(c); //自动拆箱,将一个对象的数值解析并赋给一个基本数据类型变量 int a1 = a; System.out.println(a1); Object bl = true; //安全拆"弹" if(bl instanceof Boolean){ boolean bool = (Boolean) bl; System.out.println(bool); } } }
注意:自动装箱和自动拆箱时候,都是基本数据类型和对应包装类,Integer类对象只能自动拆箱成int(也不能是short等兼容的),反过来也是如此。不要把Integer的自动拆箱成short或boolean等。如果基本数据类型对应的是包装类的子类或父类,则会自动向上或向下转型。
1.3 String类对象与基本数据类型之间的转换
前面说的是基本数据类型和包装类之间的装箱与拆箱,这里介绍的是String类对象与基本数据类型之间的转换。
包装类还可以实现基本类型变量和字符串之间的转换,除了Character之外的所有包装类都提供了一个parseXxx(String s)静态方法,用于将一个特定字符串转换成基本类型变量;Xxx表示基本的数据类型,而String类提供了多个重载valueOf()方法,用于将基本类型变量转换成字符串。
package chapter6; public class P2STest { public static void main(String[] args){ //定义两个字符串相加,但是我们想得到数学计算结果 String str1 = "-12"; String str2 = "-33"; System.out.println("str1 = " + str1 + " str2 = " + str2); //直接相加得到的是连接,并非数学计算结果 System.out.println("字符串连接结果:str1 + str2 = " + (str1 + str2)); //需要解析成数值 int a = Integer.valueOf(str1); int b = Integer.valueOf(str2); //利用parseInt解析数值 int c = Integer.parseInt(str1); int d = Integer.parseInt(str2); System.out.println("数值计算结果:str1 + str2 = " + (a + b)); System.out.println("数值计算结果:str1 + str2 = " + (c + d)); //定义两个浮点数 float f1 = 2.3f; float f2 = 3.2f; //解析成字符串 String str3 = String.valueOf(f1); String str4 = String.valueOf(f2); System.out.println("str3 = " + str3 + " str4 = " + str4); System.out.println("字符串连接结果:str3 + str4 = " + (str3 + str4)); } } 输出结果: str1 = -12 str2 = -33 字符串连接结果:str1 + str2 = -12-33 数值计算结果:str1 + str2 = -45 数值计算结果:str1 + str2 = -45 str3 = 2.3 str4 = 3.2 字符串连接结果:str3 + str4 = 2.33.2
上面我们对于将字符串转换成基本数据类型用了两种方法,一个是parseInt方法,另一个是valueOf方法,这两个方法区别是:
static int parseInt(String s):将字符串参数作为有符号的十进制整数进行分析。 static Integer valueOf(int i):返回一个保持指定的 int 值的 Integer对象。 static Integer valueOf(String s):返回保持指定的 String 的值的 Integer对象。
从返回值可以看出他们的区别parseInt()返回的是基本类型int类型值,而valueOf()返回的是包装类Integer的实例,Integer是可以使用对象方法的 而int类型就不能和Object类型进行互相转换。
String类和基本数据类型的包装类都有valueOf方法,返回的都是对应的类的实例。
1.4 打印对象和toString方法
通过类可以构建一个实例,可以将这个实例信息打出来,那么是否可以像打印普通的基本数据类型变量一样,输出实例变量的信息呢?比如说int a = 3; 那么可以通过System.out.println(a)输出a的值,对于实例变量也可以这样干吗?
package chapter6; class Person{ private String name; public Person(String name){ this.name = name; } public void info(){ System.out.println("此人的姓名是:" + name); } }; public class PrintObject { public static void main(String[] args){ Person p = new Person("张三"); System.out.println(p); System.out.println(p.toString()); } } 输出结果: chapter6.Person@1fb8ee3 chapter6.Person@1fb8ee3
从输出结果上来看输出的结果好像不是我们想要的:张三这个名字以及他的方法。而是@符号后面跟了7位16进制数字,而System.out.println方法只能在控制台输出字符串,当使用该方法输出类的实例时,实际输出的是实例对象的toString方法的返回值,也就是上面的chapter6.Person@1fb8ee3。另外我们发现上面两种输出方式的结果是一样的。
System.out.println(p);
System.out.println(p.toString());
事实上,toString是Object类里的一个实例方法,所有Java类都是Object类的子类,因此所有Java对象都有toString方法 。toString方法把调用它的对象返回一串字符串,因为字符串可以与字符串之间进行连接运算,所以,两个对象之间可以调用toString方法进行字符串连接运算,注意:对象之间进行连接运算时候,其中一个操作数(一个对象)必须显式调用toString方法,也就是不能用System.out.println(p1 + p2)这样的方式,需要使用System.out.println(p1.toString() + p2);或System.out.println(p1.toString() + p2.toString());
不过通常这样简单的连接运算是没有多少意义的。
1.5 toString方法
toString方法是一个特殊的方法,它是一个对象"自我描述"方法,该方法用于实现这样一个功能:当程序设计者直接打印该对象时,系统会输出该对象的"自我描述"信息。另外toString方法总是返回:该对象实现类的类名@hashCode值,这个返回值并非很直观的对象的描述信息,因此如果如果用户想要返回直观的对象信息,需要重写toString方法。然后像打印一个普通的基本数据类型变量一样打印一个对象。(注意:对包装类来说,System.out.println已经重写了它们的toString方法,因此可以直接输出,也就是System.out.println(new Integer("3"));这样是没有问题的)。看一个普通的类的对象输出。
package chapter6; class Apple{ private String color; private float weight; //苹果类的构造器 public Apple(String color , float weight){ this.color = color; this.weight = weight; } //重写Object的toString方法 public String toString(){ return "苹果的颜色是" + color + ",重:" + weight + "g!"; } }; public class TestToString { public static void main(String[] args){ Apple app = new Apple("Red",277.0f); //利用重写的toString方法直接输出对象的自我描述 System.out.println(app); } } 输出结果: 苹果的颜色是Red,重:277.0g!
大部分情况下toString方法被覆写时候,总是返回一个对象的属性信息,因此通常可以返回如下格式的字符串:
类名[属性名1 = 值1,属性名2 = 值2,....]
1.6 ==和equals比较运算符
Java程序中测试两个变量是否相等有两种方式,一种是利用==运算符,另一种是利用equals方法。
1.6.1 == 运算符
当使用==来判断两个变量是否相等时,如果两个变量是基本数据类型变量,那么只需要数值相等即可(不用考虑变量的编译类型(即定义类型)),如果相等就返回true。而对于引用类型变量,则要求非常严格,就是这两个引用变量必须指向同一个对象(也就是地址相等,地址相等了,值肯定相等),==判断才会返回true。所以对于基本类型变量来说==比较的是变量的数值内容,而对于引用类型对象,比较的是变量指向的对象的地址(实际上也是变量的值,因为引用变量存储的就是它指向对象的地址,地址也是一种特殊的值)。
示例说明:
package chapter6; public class TestEqual { public static void main(String[] args){ //定义两个类型不同的基础类型变量 int a = 3; float f = 3.0f; //使用==判断基本数据类型变量的相等 if(a == f){ System.out.println("a = f = " + a); }else{ System.out.println("a和f不等"); } //用构造器创建两个实例 Integer integer1 = new Integer("3"); Integer integer2 = new Integer("3"); //使用==判断两个引用类型变量是否相等 if(integer1 == integer2){ System.out.println("两个对象相同 "); }else{ System.out.println("两个对象不同"); } //自动装箱两个包装类实例,如果值相同,是同一个实例,因为Java采用了常量池技术 Integer integer3 = 3; Integer integer4 = 3; if(integer3 == integer4){ System.out.println("两个对象相同 "); }else{ System.out.println("两个对象不同"); } //用实例对象与数值比较时候,实例会自动拆箱,进行数值比较 if(integer3 == a){ System.out.println("相同 "); }else{ System.out.println("不同"); } } }
1.6.2 equals方法
在很多时候,程序判断两个引用变量是否相等,比如字符串是否相等,也希望有一种类似于变量引用的对象内容序列串是否可以像基本数据类型的值那样相等判断方法,这种判断并不要求两个对象变量是否指向同一个对象,例如字符串变量,可能只需要他们的地址值指向的字符串对象序列相同即可,这时候就可以用equals方法进行判断。
public class TestEqual{ public static void main(String[] args){ String str1 = new String("hello"); String str2 = new String("hello"); //将输出false System.out.println("str1和str2是否相等?" + (str1 == str2)); //将输出true System.out.println("str1是否equals str2?" + (str1.equals(str2))); } }
String已经重写了Object的equals()方法,Object的equals()是一个实例方法,它比较标准和==是一样的,都要求两个实例变量地址值一样,这样的equals方法没有多少意义,因此被String重写了。String的equals()方法判断两个字符串相等的标准是:只要两个字符串的字符序列相同,equals()比较返回true,否则返回false
备注:上面代码中,部分知识涉及到了常量池方面的知识。简介如下:
1.7 java常量池技术
java中的常量池技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复创建相等变量时节省了很多时间。常量池其实也就是一个内存空间,不同于使用new关键字创建的对象所在的堆空间。String类也是java中用得多的类,同样为了创建String对象的方便,也实现了常量池的技术。
测试代码如下:
public class Test{ public static void main(String[] args){ //s1,s2分别位于堆中不同空间 String s1=new String("hello"); String s2=new String("hello"); System.out.println(s1==s2);//输出false //s3,s4位于池中同一空间 String s3="hello"; String s4="hello"; System.out.println(s3==s4);//输出true } }
用new String()创建的字符串不是常量,不能在编译期就确定,所以new String()创建的字符串不放入常量池中,他们有自己的地址空间。
String 对象(内存)的不变性机制会使修改String字符串时,产生大量的对象,因为每次改变字符串,都会生成一个新的String。 java 为了更有效的使用内存,常量池在编译期遇见String 字符串时,它会检查该池内是否已经存在相同的String 字符串,如果找到,就把新变量的引用指向现有的字符串对象,不创建任何新的String 常量对象,没找到再创建新的。所以对一个字符串对象的任何修改,都会产生一个新的字符串对象,原来的依然存在,等待垃圾回收。
代码:
String a = “test”; String b = “test”; String b = b+"java";
a,b同时指向常量池中的常量值"text",b=b+"java"之后,b原先指向一个常量,内容为"test”,通过对b进行+"java" 操作后,b之前所指向的那个值没有改变,但此时b不指向原来那个变量值了,而指向了另一个String变量,内容为”text java“。原来那个变量还存在于内存之中,只是b这个变量不再指向它了。
1.8 八种基本类型的包装类和对象池
java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
一些对应的测试代码:
public class Test{ public static void main(String[] args){ //5种整形的包装类Byte,Short,Integer,Long,Character的对象, //在值小于127时可以使用常量池 Integer i1=127; Integer i2=127; System.out.println(i1==i2); //输出true //值大于127时,不会从常量池中取对象 Integer i3=128; Integer i4=128; System.out.println(i3==i4); //输出false //Boolean类也实现了常量池技术 Boolean bool1=true; Boolean bool2=true; System.out.println(bool1==bool2); //输出true //浮点类型的包装类没有实现常量池技术 Double d1=1.0; Double d2=1.0; System.out.println(d1==d2); //输出false } }
1.9 "相同的标准"与equals方法的重写
相等的标准是什么呢?
很多书上经常说equals方法是判断两个对象的值相等。这个说法是相当错误的说法,在数学中,我们判断两个数值相等是非常容易的事情,而且标准一般来说是唯一的,就是判断对应的值是否相等(如果是向量,那就是对应的元数据是否相等)。但是对于一个程序里面的方法来说则带有很强的主观性。什么叫对象的值呢?对象的值如何相等?实际上,重写equals方法就是提供自定义的相等的标准,你认为怎么是相等,那就怎样是相等,返回值是否是true由你做主!极端情况下,可以让Person对象和Dog对象相等。
例子:
package chapter6; //定义一个类 class Person{ //覆写Object的equals方法,定义相等的标准,总是返回true public boolean equals(Object obj){ return true; } }; //定义一个空Dog类 class Dog{ }; public class OverrideEqual { public static void main(String[] args){ Person p = new Person(); Dog d = new Dog(); System.out.println("Person对象是否equals Dog对象" + p.equals(d)); System.out.println("Person对象是否equals String对象" + p.equals(new String())); } }
编译运行上面的程序后,可以发现上面的结果是很荒唐的,造成这种结果的原因是由于重写Person类的equals方法时没有任何判断,无条件返回true。
我们希望两个类型相同的对象才能进行比较,并且必须关键属性相等才能相等。下面重写Person类的equals方法,让它更符合实际情况。
package chapter6; class Person{ //定义两个关键的属性 private String name; //身份证id private String id; //提供构造器 public Person(String name , String id){ this.name = name; this.id = id; } //提供判断相等的标准 public boolean equals(Object obj){ //判断条件是首先是同类的实例才能判断 //一个函数只会返回一个return值,谁在前就有优先返回的可能 if((obj != null) && (obj instanceof Person)){ Person personObj = (Person)obj; //判断两个字符串是否相等 if(this.id.equals(personObj.id)){ return true; } } return false; } }; public class OverrideEqualRight { public static void main(String[] args){ Person p1 = new Person("孙悟空","7823872348"); Person p2 = new Person("孙行者","7823872348"); Person p3 = new Person("孙悟空","23424"); System.out.println("p1和p2是否相等" + p1.equals(p2)); System.out.println("p2和p3是否相等" + p2.equals(p3)); } }
上面判定两个人是否相等的条件是两个人的身份证id是否相等,如果相等则是同一个人。
通常而言,正确地重写equals方法应该满足下列条件:
1. 自反性:对任意x,x.equals(x)一定返回true。
2. 对称性:对任意x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。
3. 传递性:对任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true
4. 一致性:对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一直是true,要么一直是false。
5. 与null比较:对任何不是null的x,x.equals(null)一定返回false
Object默认提供的equals()只是比较对象的地址,即Object类的equals方法比较的结果与==运算符比较的结果完全相同。因此,实际应用中常常需要重写equals方法,重写equals方法时,相等条件是由系统要求决定的,因此equals方法的实现也是由系统要求决定的。
需要指出的是由于instanceof运算符的特殊性,当前面对象是后面类的实例或其子类的实例都将返回true,所以实际上重写equals方法判断2个对象是否为同一个类的实例时使用instanceof是有问题的,需要改为如下代码较为合适:if(obj != null && bj.getClass == Person.class),这行代码用了反射基础。
1.10 类成员
static关键字修饰的成员就是类成员,比如类属性、类方法和静态初始化块等,static关键字不能修饰构造器。static修饰的类成员属于整个类,不是属于单个实例的。
在Java类里只能包含属性、方法、构造器、初始化、内部类和枚举类六种成员,目前已经介绍了四种,除了构造器外,static可以修饰其他5种。
类属性既可以通过类来访问,也可通过类的对象来访问。但通过类的对象来访问类属性时,实际上并不是访问该对象所具有的属性,因为当系统创建类的对象时,系统不会再为类属性分配内存,也不会再次对类属性进行初始化,也就是说,对象根本不包括对应类的类属性。通过对象访问类属性只是一种假象,通过对象访问的依然是该类的类属性,可以这样理解:当通过对象来访问类属性时,系统会在底层转换为通过该类访问类属性。
对于类方法也是一样的,实例访问方法的时候,通过类来进行间接访问。即使这个实例指向的是null
package chapter6; public class NullAccessStatic { private static void test(){ System.out.println("静态方法"); } public static void main(String[] args){ NullAccessStatic nas = null; nas.test(); } }
注意:对static关键字而言,有一条非常重要的规则:类成员不能访问实例成员。因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还未初始化,如果运行类成员访问实例成员将会引起大量错误。static修饰的方法里面不能有this和super关键字。
2 单例(Singleton)类
在大部分时候,我们把类的构造器定义成public访问权限,允许任何类自由创建该类的对象。但在某些时候,允许其他类自由创建该类的对象没有任何意义,还可能造成系统性能下降,因为创建一个对象的系统开销问题。例如系统可能只有一个窗口管理器,一个假脱机打印设备或一个数据库引擎访问点,此时如果在系统中为这些类创建多个对象就没有太大的实际意义。
如果一个类始终只能创建一个实例,则这个类被称为单例类。
根据良好封装的原则:一旦把该类的构造器隐藏起来,则需要提供一个public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用static修饰。
除此之外,该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过该对象,也就无法保证只创建一个对象。为此该类需要使用一个属性来保存曾经创建的对象,因为该属性需要被上面的静态方法访问,故该属性使用static修饰。
下面的程序可以通过控制来实现一个单例类。
package chapter6; //通过控制创建一个单实例类 class Singleton{ //创建一个缓存实例的变量 private static Singleton instance; //隐藏构造器 private Singleton(){ } //利用此方法来返回构造器返回的实例,可以控制返回的实例个数 public static Singleton getInstance(){ //如果instance还是null,则表明还没有创建对象 //如果instance不是null,则已经创建对象了,直接返回即可 if(instance == null){ instance = new Singleton(); } return instance; } } public class ClassSingleton { public static void main(String[] args){ //创建Singleton类的实例,不能直接通过构造器,需要使用getInstance这个Single类 //对外暴露的点创建对象 Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1 == s2); } } 输出结果:true
通过上面的getInstance方法提供的自定义控制(这也是封装的优势:不允许自由访问类的属性和实现细节,而是通过方法来控制合适暴露),保证Singleton类只能产生一个实例。所以在上面程序中两次产生的Singleton对象实际上是同一个对象。
3 缓存实例的不可变类
不可变类的实例的状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。毕竟多次重复创建相同对象没有多大意义,而且加大系统开销。如果可能,应该将已经创建的不可变类实例进行缓存。
缓存是软件设计中一个非常有用的模式,缓存的实现方式比较多,不同实现方式有性能差别,这里不讨论。
下面使用一个数组作为缓存池,实现一个具有实例缓存的不可变类。
算法:
1. 创建一个数组作为缓存池
2. 检查缓存池里面是否有缓存的对象,如果有,返回,如果没有,检查缓存池是否满了,如果满了,删掉缓存对象,以先进先出为原则,如果没满,缓存一个新的
package chapter6; public class CacheImmutable { private final String name; private static CacheImmutable[] cache = new CacheImmutable[10]; //记录缓存实例位置 private static int pos = 0; //构造器 public CacheImmutable(String name){ this.name = name; } public static CacheImmutable valueOf(String name){ //遍历已经缓存的对象 for(int i = 0;i < pos;i++){ //如果有相同实例,则返回 if((cache[i] != null )&&(cache[i].name.equals(name))){ return cache[i]; } } if(pos == 10){ //如果缓存已经满了,则按照先进先出原则,覆盖缓存 cache[0] = new CacheImmutable(name); pos = 1; return cache[0]; }else{ //没有查到,又没有满,则重新创建,并修改pos的位置 cache[pos++] = new CacheImmutable(name); return cache[pos - 1]; } } public boolean equals(Object obj){ if(obj instanceof CacheImmutable){ CacheImmutable ci = (CacheImmutable)obj; if(name.equals(ci.name)){ return true; } } return false; } public int hashCode(){ return name.hashCode(); } public static void main(String[] args){ CacheImmutable ci1 = CacheImmutable.valueOf("Hello"); CacheImmutable ci2 = CacheImmutable.valueOf("Hello"); System.out.println(ci1 == ci2); } }
上面的CacheImmutable类使用了一个数组来缓存该类的对象,这个数组长度为10,即该类共可以缓存10个CacheImmutable对象。当缓存池已满时,缓存池采用先进先出规则决定哪个对象将被移出缓存池。
Java提供的java.lang.Integer 类采用了与CacheImmutable类相同的处理策略,如果采用new 构造器来创建Integer对象,每次返回的都是全新的Integer对象,如果采用valueOf方法来创建Integer对象,则会缓存该方法创建的对象。