本章要点
- 包装类及其用法
- toString方法的用法
- ==和equals的区别
- static关键字的用法
- 实现单例类
- final关键字的用法
- 不可变类和可变类
- 缓存实例的不可变类
- abstract关键字的用法
- 实现模板模式
- 接口的概念和作用
- 定义接口的语法
- 实现接口
- 接口和抽象类的联系和区别
- 面向接口编程的优势
- 内部类的概念和定义语法
- 非静态内部类和静态内部类
- 创建内部类的对象
- 扩展内部类
- 匿名内部类和局部内部类
- 使用内部类实现回调
- 枚举类概念和作用
- 手动实现枚举类
- JDK1.5提供的枚举类
- 枚举类的属性,方法和构造器
- 实现接口的枚举类
- 包含抽象方法的枚举类
- 垃圾回收和对象的finalize方法
- 强制垃圾回收的方法
- 对象的软,弱和虚引用
- JAR文件的概念的用途
- 使用jar命令创建JAR文件
除了前一章所介绍的关于类,对象的基本语法,java也提供了一些高级类特性。java为8个基本类型提供了对应的包装类,通过这些包装类可以把8个基本类型的值包装成对象使用,JDK1.5提供了自动装修和自动拆箱,允许把基本类型值直接赋给对应的包装类引用变量,也允许把包装类对象直接赋给对应的基本类型变量。
java提供了final关键字类修饰变量,方法和类,系统不允许为final变量重新赋值,子类不允许覆盖父类的final方法,final类不能派生子类。通过使用final关键字,允许java实现不可变类,不可变类会让系统更加安全。
abstract和interface两个关键字分别用于定义抽象类和接口,抽象类和接口都是从多个子类中抽象出来的共同特征。但抽象类主要作为多个类的模板,而接口则定义了多类应该遵守的规范。enum关键字则用于创建枚举类,枚举类是一种不能自由创建对象的类,枚举类的对象在定义类时已经固定下来。枚举类特别适合定义像行星,季节这样的类,他们能创建的实例是有限且确定的。
本章将进一步介绍对象在内存中的运行机制,并深入介绍对象的几种引用方式,以及垃圾回收如何处理具有不同引用的对象,并详细介绍如何使用jar命令来创建JAR包。
6.1 基本数据类型的包装类
java之所以提供这8种基本数据类型,主要是为了照顾程序员传统的习惯。
6.2 处理对象
java对象都是Object类的实例。
6.2.1 打印对象和toString方法
6.2.2 ==和equals比较运算符
当使用来判断两个变量是否相等时,如果2个变量是基本类型的变量,且都是数值型(不一定要求数据类型严格相同),则只要两个变量的值相等,使用判断就将返回true。
但对于两个引用类型的变量,必须它们指向同一个对象时,==判断才会返回true,==不可比较类型上没有父子关系的两个对象。
通常而言,正确地重写equals方法应该满足下列条件:
- 自反性:对任意x,x.equals(x)一定返回true。
- 对称性:
- 传递性
- 一致性
- 对任何不是null的x,x.equals(null)一定返回false。
Object默认提供的equals()只是比较对象的地址,即Object类的equals方法比较的结果与==运算符比较的结果完全相同。因此,实际应用中常常需要重写equals方法,重写equals方法时,相等条件是由系统要求决定的,因此equals方法的实现也是由系统要求决定的。
6.3 类成员
static关键字修饰的成员就是类成员,前面已经介绍的类成员有类属性,类方法,静态初始化块等三个成分,static关键字不能修饰构造器。static修饰的类成员属于整个类,不是属于单个实例的。
6.3.1 理解类成员
在java类里只能包含属性,方法,构造器,初始化块,内部类和枚举类等六种成员,其中static可以修饰属性,方法,初始化块,内部类和枚举类,以static修饰的成员就是类成员。类成员属于整个类,而不是属于单个对象。
类属性属于整个类,当系统第一次准备使用该类时,系统会为该属性分配内存空间,类属性开始生效,直到该类被卸载,该类的类属性所占有的内存才被系统的垃圾回收机制回收。类属性生存范围几乎等同于该类的生存范围。当类初始化完成后,类属性也被初始化完成。
类属性即可通过类开访问,也可通过类的对象来访问。但通过类的对象来访问类属性时,实际上并不是访问该对象所具有的属性,因为当系统创建该类的对象时,系统不会再为类属性分配内存,也不会再次对类属性进行初始化,也就是说,对象根本不包括对应类的类属性。通过对象访问类属性只是一种假象,通过对象访问的依然是该类的类属性,可以这样理解:当通过对象来访问类属性时,系统会在底层转换为通过该类来访问类属性。
提示:
C#不允许通过对象访问类属性,对象只能访问实例属性;类属性必须通过类来访问。
由于所有对象实际上并不保持类属性,类属性是由该类来保持的,同一个类的所有对象访问类属性的,实际访问的是该类所持有的属性。因此从程序运行表面上来看,即可看到同一类的所有实例的类属性共享同一块内存区。
类方法也是类成员的一种,类方法也是属于类的,通常直接使用类作为调用者来调用类方法,但也可以使用对象来调用类方法。与类属性类似地,即使使用对象来调用类方法,其效果与采用类来调用类方法完全一样。
提示:
如果一个null对象访问实例成员(包括属性和方法),将会引发NullPointerException异常,因为null表面该实例根本不存在,既然实例不存在,理所当然的,那么它的属性和方法也不存在。
静态初始化块也是类成员的一种,静态初始化块用于执行类初始化动作,在类的初始化阶段,系统会调用该类的静态初始化块来对类进行初始化。一旦该类初始化结束后,静态初始化块将永远不会获得执行的机会。
对static关键字而言,有一条非常重要的规则:类成员(包括方法,初始化块,内部类和枚举类)不能访问实例成员(包括属性,方法,初始化块,内部类和枚举类)。因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化,如果允许类成员访问实例成员将会引起大量错误。
6.3.2 单例(Singleton)类
如果一个类始终只能创建一个实例,则这个类被称为单例类。
一旦把该类的构造器隐藏起来,则需要提供一个public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用static修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。
final 修饰符
final关键字可用于修饰类,变量和方法,final关键字有点类似C#里的sealed关键字,它用于表示它修饰的类,方法和变量不可改变。
6.4.1 final 变量
严格的说法是final修饰的变量不可被改变,一旦获得初始值之后,该final变量的值就不能被重新赋值。
final修饰成员变量
成员变量是随类初始化或对象初始化而初始化的。当类初始化时,系统会为该类的类属性分配内存,并分配默认值;当创建对象时,系统会为该对象的实例属性分配内存,并分配默认值。也就是说,当执行静态初始化块时可以对类属性赋初始值,当执行普通初始化块,构造器时可对实例属性赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,可以在初始化块,构造器中指定初始值,否则,成员变量的初始值将是由系统自动分配的初始值。
对于final修饰的成员变量而言,一旦有了初始值之后,就不能被重新赋值,因此不可以在普通方法中对成员变量重新赋值。成员变量只能在定义该成员变量时指定默认值,或者在静态初始化块,初始化块和构造器为成员变量指定初始值。如果既没有在定义成员变量时指定初始值,也没有在初始化块,构造器中为成员变量指定初始值,那么这些成员变量的值将一直是0,“\u0000”,false或null,这些成员变量也就失去了存在的意义。
因此当使用final修饰成员变量的时候,要么在定义成员变量时候指定初始值,要么在初始化块,构造器中为成员变量赋初始值。如果在定义该成员变量时指定了默认值,则不能在初始化块,构造器中为该属性重新赋值。归纳起来,final修饰的类属性,实例属性能指定初始值的地方如下:
- 类属性:可在静态初始化块中,声明该属性时指定初始值。
- 实例属性:可在非静态初始化块,声明该属性,构造器中指定初始值。
提示:
final修饰的实例属性,要么在定义该属性时指定初始值,要么在普通初始化块,或构造器中为该属性指定初始值,但需要注意的是,如果普通初始化块已经为某个实例属性指定了初始值,则不能再在构造器中为该实例属性指定初始值;final修饰的类属性,要么在定义该属性时指定初始值,要么在静态初始化块中为该属性指定初始值。实例属性不能再静态初始化块中指定初始值,因为静态初始化块是静态成员,不可访问实例属性--非静态成员;类属性不能再普通初始化块中指定初始值,因为类属性在类初始化阶段已经被初始化了,普通初始化块不能对其重新赋值。
与普通成员变量不同的是,final成员变量(包括实例属性和类属性)必须由程序员显式初始化,系统不会对final成员进行隐式初始化。所以,如果打算在构造器,初始化块中对final成员变量进行初始化,则不要在初始化之前就访问成员变量的值。
final 修饰局部变量
系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final修饰局部变量时既可以在定义时指定默认值,也可以不指定默认值。
如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中进行该final变量赋初始值,但只能一次,不能重复赋值;如果final修饰的局部变量在定义时已经指定默认值,则后面代码中不能再对该变量赋值。
final修饰基本类型和引用类型变量的区别
当使用final修饰基本类型变量时,,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型的变量而言,它保存的即仅仅是一个引用,final只保证这个引用所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
使用final修饰的引用类型变量不能被重新赋值,但可以改变引用型变量所引用对象的内容。
注意:
如果final修饰的变量是基本数据类型,且在编译时就可以确定该变量的值,于是可以把该变量当成常量处理。根据java的可读性命名规范:常量名由多个有意义的单词连缀而成,每个单词的所有字母全部大写,单词与单词之间以下划线来分隔,例如MAX_TAX_RATE=20.反之,如果final修饰的变量是引用数据类型,final变量无法再编译时就获得值,而必须在运行时才能得到值。
6.4.2 final 方法
final修饰的方法不可被重写,如果处于某些原因,不希望子类重新父类的某个方法,则可以使用final修饰该方法。
6.4.3 final 类
final修饰的类不可有子类。
当子类继承父类时,将可以访问到父类内部数据,并可通过重写父类方法来改变父类方法的实现细节,这可能导致一些不安全的因素。为了保证某个类不可以被继承,则可以使用final修饰这个类。
6.4.4 不可变类
不可变类的意思是创建该类的实例后,该实例的属性是不可改变的。java提供的8个包装类和java.lang.String都是不可变类,当创建它们的实例后,其实例的属性不可改变。
如果需要创建自定义的不可变类,可遵守如下规则:
- 使用private和final修饰符来修饰该类的属性。
- 提供带参数构造器,用于根据传入参数来初始化类里的属性。
- 仅为该类的属性提供getter方法,不要为该类的属性提供setter方法,因为普通方法无法修改final修饰的属性。
- 如果有必要,重写Object类中hashCode和equals方法。在equals方法根据关键属性来作为两个对象相等的标准,除此之外,还应该保证两个用equals方法判断为相等的对象的hashCode也相等。
6.4.5 缓存实例的不可变类
不可变类的实例的状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。毕竟多次重复创建相同对象没有太大的意义,而且加大系统开销。如果可能,应该将已经创建的不可变类实例进行缓存。
缓存是软件设计中一个非常有用的模式,缓存的实现方式有很多,不同实现方式可能存在较大的性能差别,关于缓存的性能问题此处不做深入讨论。
6.5 抽象类
当编写一个类时,常常会为该类定义一些方法,这些方法用以描述该类的行为方式,那么这些方法都有具体的方法体。但在某些情况下,某个父类只是知道其子类应该包含怎样的方法,但无法准确知道这些子类如何实现这些方法,例如定义了一个Shape类,这个类应该提供一个计算周长的方法calPerimeter(),但不同Shape子类对周长的计算方法是不一样的,即Shape类无法准确知道其子类计算周长的方法。
如何既能让Shape类里包含calPerimeter()方法,但又无需提供其方法实现呢?使用抽象方法即可满足该要求:抽象方法是只有方法签名,没有方法实现的方法。
6.5.1 抽象方法和抽象类
抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。
抽象方法和抽象类的规则如下:
- 抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。
- 抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
- 抽象类可以包含属性,方法(普通方法和抽象方法都可以),构造器,初始化块,内部类,枚举类六种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
- 含有抽象方法的类(包括直接定义了一个抽象方法:继承了一个抽象父类,但没有完全实现父类包含的抽象方法;以及实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。
注意:
根据上面定义规则,不难发现抽象类同样能包含和普通类相同的成员。只是抽象类不能用于创建实例;普通类不能包含抽象方法,而抽象类可以包含抽象方法。
定义抽象方法只需在普通方法上增加abstract修饰符,并把普通方法的方法体(也就是方法后花括号括起来的部分)全部去掉,并在方法后增加分号即可。
注意:
抽象方法和空方法体的方法不是同一个概念。
抽象类不能用于创建实例,只能被当做父类被其他子类继承。
当abstract修饰类时,表面这个类只能被继承,当abstract修饰方法时,表明这个方法必须由子类提供实现(即重写)。而final修饰的类不能被继承,final修饰的方法不能被重写。因此final和abstract永远不能同时使用。
注意:
abstract不能用于修饰属性,不能用于修饰局部变量,即没有抽象变量,没有抽象属性等说法;abstraabstract也不能用于修饰构造器,没有抽象构造器。抽象类里定义的构造器只能是普通构造器。
除此之外,当使用static来修饰一个方法时,表面这个方法属于当前类,即该方法可以通过类来调用,如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误(调用了一个没有方法体的方法肯定会引起错误),因此static和abstract不能同时修饰某个方法,即没有所谓的类抽象方法。
abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此abstract方法不能定义为private访问权限,即private和abstract不能同时使用。
6.5.2 抽象类的作用
从前面的示例程序可以看出,抽象类不能创建实例,它只能当成父类来被继承。从语义的角度来看,抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展,改造,但子类总体上会大致保留抽象类的行为方式。
如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,这就是一种模板模式,模板模式也是最常见,最简单的设计模式之一。
模板模式在面向对象的软件中很常用。其原理简单,实现也很简单。下面是使用模板模式的一些简单规则:
- 抽象父类可以只定义需要使用的某些方法,其余则留给其子类实现。
- 父类中可能包含需要调用的其他系列方法的方法,这些被调方法既可以由父类实现,也可以由其子类实现。父类里提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,而必须依赖于其子类的辅助。
6.6 更彻底的抽象:接口
抽象类是从多个类中抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的“抽象类”--接口(interface),接口里不能包含普通方法,接口里所有方法都是抽象方法。
6.6.1 接口的概念
读者可能经常听说接口,比如PCI接口,AGP接口等,因此很多读者认为接口等同于主机板上的插槽,这其实是一种错误的认识。当我们说PCI接口时,指的是主机板上那条插槽遵守了PCI规范,而具体的PCI插槽只是PCI接口的实例。
对于不同型号的主机板而言,它们各自的PCI插槽都需要遵守一个规范,遵守这个规范就可以保证插入该插槽里的板卡能与主机板正常通信。对于同一个型号的主机板而言,它们的PCI插槽需要有相同的数据交换方式,相同的实现细节,它们都是同一个类的不同实例。
同一个类的内部状态数据,各种方法的实现细节完全相同,类是一种具体实现体而接口定义了一种规范,接口定义某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节。它只规定这批类里必须提供某些方法,提供者这些方法的类就可满足实际需要。
可见,接口是从多个相似类中抽象出来的规范,接口不提供任何实现。接口体现的是规范和实现分离的设计哲学。
让规范和实现分离正是接口的好处,让软件系统的各组件之间面向接口耦合,是一种松耦合的设计。例如主机板上提供了PCI插槽,只要一块显卡遵守PCI接口规范,就可以插入PCI插槽内,与该主机板正常通信。至于这块显卡的是哪个厂家制造的,内部是如何实现的,主机板无须关心。
类似的,软件系统的各模块之间也应该采用这种面向接口的耦合,从而尽量降低各模块之间耦合,为系统提供更好的可扩展性和可维护性。
因此接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这就意味着接口里通常是定义一组公用方法。
6.6.2 接口的定义
[修饰符] interface 接口名 extends 父接口1,父接口2...
{ 零个到多个常量定义... 零个到多个抽象方法定义... }
上面语法的详细说明如下:
- 修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包权限访问控制符,即只有在相同包结构下才可以访问该接口。
- 接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可;如果要遵守java可读性规范,则接口名应由多个有意义的单词连缀而言,每个单词首字母大写,单词与单词之间无需任何分隔符。
- 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含属性(只能是常量),方法(只能是抽象实例方法),内部类(包括内部接口)和枚举类定义。
对比接口和类的定义方式,不难发现接口的成员比类里的成员少了2中,而且接口里的属性只能是常量,接口里的方法只能是抽象方法。
前面已经说过了,接口里定义的是多个类共同的公共行为规范,因此接口里所有成员,包括常量,方法,内部类和枚举类都是public访问权限。定义接口成员时,可以省略访问控制符,如果指定访问控制修饰符,只能使用public访问控制修饰符。
对于接口里定义的常量属性而言,他们是接口相关的,而且他们只能是常量,因此系统会自动为这些属性增加static和final两个修饰符。也就是说,在接口定义属性时,不管是否使用public static final修饰符,接口里的属性总将使用这三个修饰符来修饰。而且,接口里没有构造器和初始化块,因此接口里定义的属性只能在定义时指定默认值。
接口里定义的内部类和枚举类,他们默认都采用public static两个修饰符,不管定义时是否指定这两个修饰符,系统自动使用public static对他们进行修饰。
注意:
从某个角度来看,接口可被当成一个特殊的类,因此一个java源文件里最多只能有一个public接口,如果一个java源文件里定义了一个public接口,则该源文件的主文件名必须与该接口名相同。
6.6.3 接口的继承
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法,常量属性,内部类和枚举类定义。
一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号(,)隔开。
6.6.4 使用接口
接口不能用于创建实例,但接口可以用于声明引用类型的变量。当使用接口来声明引用类型的变量时,这个引用类型变量必须引用到其他实现类的对象。除此之外,接口的主要用途就是被实现类实现。
一个类可以实现一个或多个接口,继承使用extends关键字,实现则使用implements关键字。因为一个类可以实现多个接口,这也是java为单继承灵活性不足所做的补充。
一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。
注意:
实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只是只能更大或者相等,所以实现类实现接口里的方法时只能使用public访问控制权限。
接口不能显示继承任何类但所有接口类型的引用变量都可以直接赋给Object类型的引用变量。
6.6.5 接口和抽象类
接口和抽象类很像,它们都具有如下特征:
- 接口和抽象类都不能被实例化,他们都位于继承树的顶端,用于被其他类实现和继承。
- 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(就是如何来调用方法)。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
从某种程度上来看,接口类似于整个系统的“总纲”,它制定了系统各模板应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。
抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,必须有更进一步的完善,这种完善可能有几种不同方式。
除此之外,接口和抽象类在用法上也存在如下差别:
- 接口里只能包含抽象方法,不包含已经提供实现的方法;抽象类则完全可以包含普通方法。
- 接口里不能定义静态方法;抽象类里可以定义静态方法。
- 接口里只能定义静态常量属性,不能定义普通属性;抽象类里既可以定义普通属性,也可以定义静态常量属性。
- 接口不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而让其子类调用这些构造器来完成属于抽象类的初始化操作。
- 接口里不能包含初始化块,但抽象类则完全可以包含初始化块。
- 一个类最多只能有一个直接父类包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补java单继承的不足。
6.6.6 面向接口编程
接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。
基于这种原则,很多软件架构设计理论都倡导“面向接口”编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。
简单工厂模式
上面介绍的就是一种被称为“简单工厂”的设计模式。所谓设计模式,就是对经常出现的软件设计问题的成熟解决方案。很多人把设计模式想象成非常高深的概念,实际上设计模式仅仅是对特定问题的一种惯性思维。
命令模式
考虑这样一种场景:某个方法需要完成某一个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。
6.7 内部类
大部分时候,我们把类定义成一个独立的程序单元。在某些情况下,我们把一个类放在另一个类的内部定义这个定义在其他类内部的类就被称为内部类(有的地方也叫嵌套类),包含内部类的类也被称为外部类(有的地方也叫宿主类)。内部类主要有如下作用:
- 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
- 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其他外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的属性。
- 匿名内部类适合用于创建那些仅需要一次使用的类。
6.7.1 非静态内部类
定义内部类非常简单,只要把一个类放在另一个类内部定义即可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类被称为局部内部类)。
大部分时候,内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与属性,方法,构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。
成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。
注意:
外部类的上一级程序单元是包,所以它只有2个作用域:同一个包和任何位置。因此只需两种访问权限:包访问权限和公开访问权限。正好对应省略访问控制符和public访问控制符。省略访问控制符是包访问权限,即同一包中的其他类可以访问省略访问控制符的成员。因此,如果一个外部类不使用任何访问控制符修饰,则只能被同一个包中其他类访问。而内部类的上一级程序单元是外部类,它就具有四个作用域:同一个类,同一个包,父子类和任何位置,因此可以使用四种访问控制权限。
非静态内部类的成员可以访问外部类的private成员,但反过来就不成立了。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问非静态内部类成员,则必须显式创建非静态内部类对象来调用访问其实例成员。
非静态内部类对象和外部类对象的关系是怎样的?
非静态内部类对象必须寄存在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄存其中。简单地说,如果存在一个非静态内部类对象,则一定存在一个被它寄存的外部类对象。但外部类对象存在时,外部类对象里不一定寄存了非静态内部类对象。因此外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在!而非静态内部类对象访问外部类成员时,外部类对象一定是存在的。
根据静态成员不能访问非静态成员的规则,外部类的静态方法,静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量,创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。
java不允许在非静态内部类里定义静态成员。
非静态内部类里不能有静态方法,静态属性,静态初始化块。
注意:
非静态内部类里不可以有静态初始化块,但可以包含普通初始化块。非静态内部类普通初始化块的作用与顶层类初始化块的作用完全相同。
6.7.2 静态内部类
如果使用static来修饰一个内部类,则这个内部类变成是外部类类相关的,属于整个外部类,而不是单独属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也被称为静态内部类。
注意;
static关键字的作用是把类的成员变成类相关,而不是实例相关,即static修饰成员是属于整个类,而不是属于单个对象。外部类的上一级程序单元是包,所以不可使用static修饰;而外部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关。因此static关键字不可修饰外部类,可以修饰内部类。
静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,所以静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。
为什么静态内部类实例方法也不能访问外部类的实例属性呢?
因为静态内部类是外部类的类相关,而不是外部类的对象相关的。也就是说,静态内部类的对象不是寄存在外部类对象里,而是寄存在外部类的类本身中。也就是说,当静态内部类的对象存在时,并不存在一个被它寄存的外部类对象,静态内部类的对象里只有外部类的类引用,没有外部类对象的引用。如果允许静态内部类的实例方法访问外部类的实例成员时,但找不到被寄存的外部类对象,这将引起错误。
6.7.3 使用内部类
定义类的主要作用就是定义变量,创建实例和作为父类被继承。定义内部类的主要作用也如此。但使用内部类定义变量和创建实例则与外部类存在一些小小的差异。下面分三种情况讨论内部类的用法。
在外部类内部使用内部类
从前面程序中可以看出,在外部类的内部使用内部类时,与平常使用普通类没有太大的区别。一样可以直接通过内部类类名来定义变量,通过new调用内部类构造器来创建实例。
唯一存在的一个区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。
在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。
在外部类以外使用非静态内部类
如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。对于使用其他访问控制符修饰的内部类,则能在访问控制符对应访问权限内使用。
- 省略访问控制符的内部类,只能被与外部类处于同一个包中其他类所访问。
- 使用protected修饰的内部类:可被与外部类处于同一个包中其他类和外部类的子类所访问。
- 使用public修饰的内部类:可在任何地方被访问。
注意:
非静态内部类的子类不一定是内部类,它可以是一个顶层类。但非静态内部类的子类的实例一样需要保留一个引用,该引用指向其父类所在外部类的对象。也就是说,如果有一个内部类子类的对象存在,则一定存在与之对应外部类的对象。
在外部类以外使用静态内部类
因为静态内部类是外部类类相关的,因此创建内部类对象时无需创建外部类的对象。
不管是静态内部类,还是非静态内部类,它们声明变量的语法完全一样。区别只是在创建内部类对象时,前者只需使用外部类来调用构造器,而后者必须使用外部类对象来调用构造器。
既然内部类是外部类的成员,是否可以为外部类定义子类,在子类中再定义一个内部类来重写其父类中的内部类?
不可以!从上面知识可以看出,内部类的类名不再是简单地由内部类的类名组成,它实际上还把外部类作为一个命名空间,作为内部类类名的限制。因此子类中内部类和父类中的内部类不可能完全同名,即使二者所包含的内部类的类名相同,但因为它们所处的外部类空间不同,所以它们不可能是同一个类,也就不可能重写。
6.7.4 局部内部类
如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。因此,局部内部类不能在外部类以外的地方使用,那么局部内部类也不能使用访问控制符和static修饰符修饰。
注意:
对于局部成员而言,不管是局部变量,还是局部内部类,它们的上一级程序单元是方法,而不是类,使用static修饰它们没有任何意义。因此,所有局部成员都不能使用static修饰。不仅如此,因为局部成员的作用域是所在方法,其他程序单元永远也不可能访问另一个方法中的局部成员,所以所有局部成员都不能使用访问控制符修饰。
6.7.5 匿名内部类
匿名内部类适合创建那种只需要一次使用的类,例如前面介绍命令模式时所需要的Command对象。
- 匿名内部类不能是抽象类,因为系统在创建匿名内部类的时候,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
- 匿名内部类不能定义构造器,因为匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义实例初始化块,通过实例初始化块来完成构造器需要完成的事情。
6.7.6 闭包(Closure)和回调
闭包是一种能被调用的对象,它保存了创建它的作用域的信息。
6.8 枚举类
在某些情况下,一个类的对象是有限而且固定的。这种实例有限而且固定的类,在java里被称为枚举类。
6.8.1 手动实现枚举类
如果需要手动实现枚举类,可以采用如下设计方式:
- 通过private将构造器隐藏起来。
- 把这个类的所有可能实例都使用public static final属性来保存。
- 如果有必要,可以提供一些静态方法,允许其他程序根据特定参数来获取与之匹配实例。
6.8.2 枚举类入门
J2SE1.5 新增了一个enum关键字,用以定义枚举类。正如前面看到,枚举类是一种特殊的类,它一样可以有自己的方法和属性,可以实现一个或者多个接口,也可以定义自己的构造器。一个java源文件中最多只能定义一个public访问权限的枚举类,且该java源文件也必须和该枚举类的类名相同。
但枚举类终究不是普通类,它与普通类有如下简单区别:
- 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。
- 枚举类的构造器只能使用private访问控制符,如果省略了其构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。
- 枚举类的所有实例必须在枚举类中显式列出,否则这个枚举类将永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。
- 所有枚举类都提供了一个values方法,该方法可以很方便地遍历所有的枚举值。
所有的枚举类都继承了java.lang.Enum类,所以枚举类可以直接使用java.lang.Enum类中所包含的方法。java.lang.Enum类中提供了如下几个方法:
- int compareTo(E o):该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回零;
- String name():返回此枚举实例的名称,这个名字就是定义枚举类时列出的所有枚举值之一。于此方法相比,大多数程序员应该优先考虑使用toString()方法,因为toString()方法返回更加用户友好的名称。
- int ordinal():返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)。
- String toString():返回枚举常量的名称,大致上与name方法相似,但toString()方法更常用。
- public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name):这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。
6.8.3 枚举类的属性,方法和构造器
枚举类也是一种类,只是它是一种比较特殊的类,因此它一样可以使用属性和方法。
6.8.4 实现接口的枚举类
枚举类也可以实现一个或多个接口。与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包含的方法。
如果由枚举类来实现接口里的方法,则每个枚举值在调用该方法时,都有相同的行为方式(因为方法体完全一样)。如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法,每个枚举值提供不同的实现方式,从而让不同枚举值调用该方法时具有不同的行为方式。
6.8.5 包含抽象方法的枚举类
枚举类里定义抽象方法时无需显式使用abstract关键字将枚举类定义成抽象类,但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。
6.9 对象与垃圾回收
java的垃圾回收是java语言的重要功能之一。当程序创建对象,数组等引用类型实体时,系统都会在堆内存中为之分配一块内存区,对象就保存在这款内存区中,当这块内存不再被任何引用变量引用时,这块内存就变成垃圾,等待垃圾回收机制进行回收。垃圾回收机制具有如下特征:
- 垃圾回收机制只负责回收堆内存中对象,不会回收任何物理资源(例如数据库连接,网络IO等资源)。
- 程序无法精确控制垃圾回收的运行,垃圾回收会在合适时候进行。当对象永久性地失去引用后,系统就会在合适时候回收它所占的内存。
- 垃圾回收机制回收任何对象之前,总会先调用它的finalize方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。
6.9.1 对象在内存中的状态
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:
- 激活状态:当一个对象被创建后,有一个以上的引用变量引用它,则这个对象在程序中处于激活状态,程序可通过引用变量来调用该对象的属性和方法。
- 去活状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了去活状态。在这个状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有去活状态对象的finalize方法进行资源清理,如果系统在调用finalize方法让一个引用变量引用该对象,则这个对象会再次变为激活状态;否则该对象将进入死亡状态。
- 死亡状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize方法依然没有使该对象变成激活状态,那这个对象将永久性地失去引用,最后变成死亡状态。
只有当一个对象处于死亡状态时,系统才会真正回收该对象所占有的资源。
一个对象可以被一个方法局部变量所引用,也可以被其他类的类属性引用,或被其他对象的实例属性引用。当某个对象被其他对象的实例属性引用时,只有当该对象被销毁后,该对象才会进入去活状态。
6.9.2 强制垃圾回收
当一个对象失去引用后,系统何时调用它的finalize方法对它进行资源清理,何时它会变成死亡状态,系统何时回收它所占有的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,绝不能控制它何时被回收。
程序无法精确控制java垃圾回收的时机,但我们依然可以强制系统进行垃圾回收--只是这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时,程序强制系统垃圾回收后总会有一些效果。强制系统垃圾回收有如下两个方法:
- 调用System类的gc()静态方法:System.gc()
- 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()
虽然图中显示了程序强制垃圾回收的效果,但这种强制仅仅只是建议系统立即进行垃圾回收,系统完全有可能并不立即进行垃圾回收,但垃圾回收机制也不会对程序的建议完全置之不理;垃圾回收机制会在收到通知后,尽快进行垃圾回收。
6.9.3 finalize 方法
当垃圾回收机制回收某个对象所占用的内存之前,通常要求程序调用适当的方法来清理资源,在没有明确指定资源清理的情况下,java提供了默认机制来清理该对象的资源,这个方法就是finalize。
当finalize()方法返回之后,对象消失,垃圾回收机制开始执行。方法原型中的throws Throwable表示它可以抛出任何类型的异常。
任何java类都可以覆盖Object类的finalize方法,在该方法中清理该对象来清理资源。如果程序终止之前始终没有进行垃圾回收,则不会调用失去引用对象的finalize方法来清理资源。垃圾回收机制何时调用对象的finalize方法是完全透明的,只有当程序认为需要更多额外内存时,垃圾回收机制才会进行垃圾回收。因此,完全有可能出现一种情形:某个失去引用对象只占用了少量内存,而且系统没有产生严重的内存需求,因此垃圾回收机制并没有试图回收该对象所占用的资源,所以该对象的finalize方法也不会得到调用。
finalize方法有如下四个特点:
- 永远不要主动调用某个对象的finalize方法,该方法交给垃圾回收机制调用。
- finalize方法何时被调用,是否被调用具有不确定性。不要把finalize方法当成一定会被执行的方法。
- 当JVM执行去活对象的finalize方法时,可能使该对象或系统中其他对象重新变成激活状态。
- 当JVM执行finalize方法时出现了异常,垃圾回收机制不会报告异常,程序继续执行。
注意:
由于finalize方法并不一定会被执行,如果想保证某个类里打开的资源被清理,不要放在finalize方法中进行清理,后面内容有介绍专门用于进行资源清理的方法。
6.9.4 对象的软、弱和虚引用
对于大部分对象而言,程序里会有一个引用变量引用该对象,这种引用方式是最常见的引用方式。除此之外,java.lang.ref包下提供了三个类:SoftReference,PhantomReference和WeakReference,它们分别代表了系统对对象的三种引用方式:软引用,虚引用和弱引用。因此,java语言对对象的引用有如下四种。
强引用(StrongReference)
这是java程序中最常见的引用方式,程序创建一个对象,并把这个对象赋给一个引用变量。程序通过该引用变量来操作实际的对象,前面介绍的对象和数组都是采用了这种强引用的方式。当一个对象被一个或一个以上的引用变量所引用时,它处于激活状态,不可能被系统垃圾回收机制回收。
软引用(SoftReference)
软引用需要通过SoftReference类来实现,当一个对象只具有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统将会回收它。软引用通常用于对内存敏感的程序中。
弱引用(WeakReferece)
弱引用通过WeakReference类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收-正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
虚引用(PhantomReference)
虚引用通过PhantomReerence类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。
上面三个引用类都包含了一个get方法,用于获取被它们所引用的对象。
引用队列由java.lang.ref.ReferenceQueue类表示,它用于保存被回收后对象的引用。当把软引用,弱引用和引用队列联合使用时,系统在回收被引用的对象之后,将把被回收对象对应的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被释放之前,将把它对应的虚引用添加到它的关联的引用队列中,这使得可以在对象被回收之前采取行动。
软引用和弱引用可以单独使用,但虚引用不能单独使用,单独使用虚引用没有太大意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含了该虚引用,从而了解虚引用所引用对象是否即将被 回收。
使用这些引用类可以避免在程序执行期间将对象留在内存中。如果我们以软引用,弱引用或虚引用的方式引用对象,这样垃圾收集器就能够随意地释放对象。如果希望尽可能减小程序在其生命周期中所占用的内存大小时,这些引用类就很有好处。
当然必须指出:要使用这些特殊的引用类,就不能保留对对象的强引用。如果保留了对对象的强引用,那么就会浪费这些类所提供的任何好处。
由于垃圾回收的不确定性,当程序希望从弱引用中取出被引用对象时,可能这个被引用对象已经被释放了。如果程序需要使用那个被引用的对象,则必须重新创建该对象。
6.10 修饰符的适用范围
native关键字主要用于修饰一个方法,使用native修饰的方法类似于一个抽象方法。与抽象方法不同的是,native方法通常采用C语言来实现。如果某个方法需要利用平台相关特性,或者访问系统硬件等,则可以把该方法使用native修饰,再把该方法交给C去实现。一旦java程序中包含了native方法,这个程序将失去跨平台的功能。
四个访问控制符是互斥的,最多只能出现其中之一。不仅如此,还有abstract和final不能同时使用,abstract和static不能同时使用,abstract和private不能同时使用。
6.11 使用JAR文件
JAR文件的全称是Java Archive File,意思就是java档案文件。通常JAR文件是一种压缩文件,与我们常见的ZIP压缩文件兼容,通常也被称为JAR包。JAR文件与ZIP文件的区别就是在JAR文件中默认包含了一个名为META-INF/MANIFEST.MF的清单文件,这个清单文件是在生成JAR文件时由系统自动创建的。
当开发了一个应用程序后,这个应用程序包含了很多类,如果需要把这个应用程序提供给别人使用,通常会将这些类文件打包成一个JAR文件,把这个JAR文件提供给别人使用。只要别人在它的CLASSPATH环境变量中添加这个JAR文件,则Java虚拟机就可以自动在内存中解压这个JAR包,把这个JAR文件当成一个路径,在这个路径中查找所需要的类或包层次对应的路径结构。
使用JAR文件有一下好处:
- 安全。能够对JAR文件进行数字签名,只让能够识别数字签名的用户使用里面的东西。
- 加快下载速度。在网上使用Applet时,如果存在多个文件而不打包,为了能够把每个文件都下载到客户端,需要为每个文件单独建一个HTTP连接,这是非常耗时的工作。使用这些文件压缩成一个JAR包,则只要建立一次HTTP连接就能够一次下载所有文件。
- 压缩。使文件变小,JAR的压缩机制和ZIP完全相同。
- 包封装。能够让JAP包里面的文件依赖于统一版本的类文件。
- 可移植性。JAR包作为内嵌在java平台内部处理的标准,能够在各种平台上直接使用。
把一个JAR文件添加到系统ClASSPATH环境变量中后,java将会把这个JAR文件当成一个路径来处理。实际上JAR文件就是一个路径,JAR文件通常使用jar,当时用jar命令压缩生产JAR文件时,可以把一个或多个路径全部压缩成一个JAR文件。