Java程序设计8——抽象类、接口与内部类
1 抽象类
当编写一个类时,常常会为该类定义一些方法,这些方法用以描述该类的行为方式,那么这些方法都有具体的方法体。但在某些情况下,某个父类并不需要实现,因为它只需要当做一个模板,而具体的实现,可以由它的子类来实现。比如说一个长方体的表面积和一个立方体的表面积计算方式是有区别的,长方体表面积需要有三个参数,而立方体需要一个参数。
抽象方法可以只有方法签名,而没有方法实现。
1.1 抽象方法和抽象类的定义
抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。
抽象方法和抽象类的规则如下:
1.抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。连大括号都不能用
2.抽象类不能被实例化(实例化是为了调用属性和方法,抽象类本身没有方法实现的),无法使用new关键字来调用抽象类的构造器来初始化抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
3.抽象类可以包含属性、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类、枚举类六种成分。抽象类的构造器不能用于创建实例,主要用于被其子类调用
4.含义抽象方法的类(包括直接定义了一个抽象方法:继承了一个抽象父类,但没有完全实现父类包含的抽象方法;以及实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。
根据定义规则:普通类不能包含抽象方法,而抽象类不仅可以包含普通方法,也可以包含抽象方法。
abstract class 类名称{ 属性; 权限类型 返回值类型 方法名称(参数类别 参数列表){ } 权限类型 abstract 返回值类型 抽象方法名称(参数类型 参数列表) }
注意些抽象方法时候:只能是声明,后面连大括号{}都不能跟。大括号里面哪怕什么都没有,也表示是空语句,不是抽象方法。
一个抽象类不能用final来声明,因为抽象类是需要继承来覆写抽象类的方法,但是final关键字意味着抽象类不能有子类,显然矛盾。
抽象类的抽象方法也不能用private声明,因为抽象类的抽象方法需要子类覆写,使用private修饰的话,子类无法覆写抽象方法。
抽象方法和空方法体的方法不是同一个概念。例如public abstract void test();是一个抽象方法,它根本没有方法体,即方法定义后面没有一对花括号;但public void test(){}方法是一个普通方法,它已经定义了一个方法体,只是方法体为空,方法体上面也不做,这个方法不能用abstract来修饰。
abstract不能用于修饰属性,不能用于修饰局部变量,即没有抽象属性、抽象变量的说法,abstract也不能用于修饰构造器,抽象类里定义的构造器只能是普通构造器。
除此之外,当使用static来修饰一个方法时,表面这个方法属于当前类,即该方法可以通过类来调用,如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误(调用了一个没有方法体的方法肯定会引起错误),因此static和abstract不能同时修饰某个方法,即没有所谓的类抽象方法。
abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此abstract方法不能定义为private访问权限,即private和abstract不能同时使用。
package chapter6; import java.util.Scanner; public class Circle extends Shape{ private double radius; public Circle(String color,double radius){ super(color); if(radius > 0){ this.radius = radius; } } public void setRadius(double radius){ if(radius > 0){ this.radius = radius; } } //重写计算园周长的方法 public double calPerimeter(){ return 2 * Math.PI * radius; } public String getType(){ return getColor() + "圆"; } public static void main(String[] args){ Triangle s1 = new Triangle("红色",3,4,5); Circle s2 = new Circle("紫色",5); System.out.println(s1.getType() + " 周长是" + s1.calPerimeter()); System.out.println(s2.getType() + " 周长是" + s2.calPerimeter()); } }
1.2 抽象类的作用
从前面的实例可以看出,抽象类不能创建实例,它只能当成父类被继承。从语义角度来看,抽象类是从多个具体类抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。
抽象类提醒的就是一种模板模式的设计,抽象类作为子类的模板,子类在抽象类的继承上进行扩展和改造。
如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给子类实现,这就是一种模板模式,模板模式也是最常见、最简单的设计模式之一。如前面的Shape、Circle和Triangle就是使用这种模式。
2 接口:更彻底的抽象
抽象类是从多个类抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的"抽象类"——接口(interface),接口里不能包含普通方法,接口里所有方法都是抽象方法。
2.1 接口的概念
经常听说接口,比如PCI接口、AGP接口,因此很多人认为接口等同于主机板上的插槽,这是一种错误的认识。当我们说PCI接口时,指的是主机板上那条插槽遵守的了PCI规范,而具体的PCI插槽只是PCI接口的实例。
对于不同型号的主机板而言,它们各自的PCI插槽都需要遵守一个规范,遵守这个规范就可以保证插入该插槽里的板卡能与主机板正常通信。对于同一个型号的主机板而言,它们的PCI插槽都需要相同的数据交换方式、相同的实现细节,它们都是同一个类的不同实例。
下图展示了三种层次的关系:接口层次、类层次和对象层次
从上图可以看出,同一个类的内部状态数据,各种方法的实现细节完全相同,类是一种具体实现体。而接口定义了一种规范,接口定义某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不需要关系这些类里方法的实现细节。它只规定这批类里必须提供某些方法,提供这些方法的类就可满足实际需要。
可见,接口是从多个相似类中抽象出来的规范,接口不提供任何实现。接口体现的是规范和实现分离的设计这些。
让规范和实现分离是接口的好处,让软件系统的各组件之间面向接口耦合,是一种松耦合的设计。例如主机板上提供了PCI插槽,只要一块显卡遵守PCI接口规范,就可以插入PCI插槽内,与该主板正常通信。至于这块显卡是哪个厂家制造的,内部如果实现,主机板无需关心。
类似的,软件系统的各模块之间也应该采用面向接口的耦合,尽量降低各模块之间的耦合,为系统提供更好的可扩展性和可维护性。
因此接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这就意味着接口里通常是定义一组共用方法。
2.2 接口的定义
和类定义不同,定义接口不再使用class关键字,而是使用interface关键字,接口定义的基本语法是:
修饰符 interface 接口名 extends 父接口1,父接口2...{ 零到多个常量定义... 领个到多个抽象方法定义... }
语法的详细说明如下:
1.修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包权限访问控制符,即只有在相同包结构下才可以访问该接口。
2.接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要合法即可,但实际是需要是有意义。
3.一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
接口是一种规范,因此接口里不能包含构造器和初始化块定义,接口里只能包含常量属性、抽象实例方法(不能有static)、内部类(包括内部接口)和枚举类定义。
由于接口是一种公共规范,因此接口里面的所有成员包括常量、抽象方法、内部类和枚举类都是public访问权限。定义接口成员时候,可以省略控制修饰符,但如果指定,则只能是public。对于接口里定义的常量属性而言,它们是接口相关的,它们只能是常量,因此系统会自动为这些属性增加static和final两个修饰符。也就是说,在接口定义属性时,不管是否使用publicstatic final修饰符,接口里的属性总将使用这三个修饰符来修饰。而且由于接口里没有构造器和初始化块,因此接口里定义的属性只能定义时指定默认值。
接口里定义属性采用如下两行代码的结果完全一样:
int MAX_SIZE = 50; public static final int MAX_SIZE = 50;
接口定义的抽象方法修饰符是public abstract,不能有static修饰,内部类和枚举类都默认采用public static两个修饰符,不管定义时是否只能这两个修饰符,系统自动使用public static 对它们进行修饰。
package chapter6; public abstract interface Output { //接口定义的属性只能是常量 int MAX_CACHE_LINE = 50; //接口里定义只能是抽象方法 void out(); void getData(String name); } package chapter6; public class TestOutputProperty { public static void main(String[] args){ //访问另一个包中的属性 System.out.println(chapter6.Output.MAX_CACHE_LINE); //不能重新赋值,因为接口的属性都是final的 //Output.MAX_CACHE_LINE = 3; } }
2.3接口的继承
接口的继承和类继承不同,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将获得父接口的所有抽象方法、常量属性、内部类和枚举类定义。
一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号隔开。
package chapter6; interface interfaceA{ int PROP_A = 5; void test_A(); } interface interfaceB{ int PROP_B = 6; void test_B(); } interface interfaceC extends interfaceA,interfaceB{ int PROP_C = 6; } public class TestInterfaceExtends { public static void main(String[] args){ System.out.println(interfaceA.PROP_A); System.out.println(interfaceB.PROP_B); System.out.println(interfaceC.PROP_C); } }
2.4 使用接口
接口不能用于创建实例,但接口可以用于声明引用类型的变量。当使用接口来声明引用类型的变量时,这个引用类型的变量必须引用到其实现类的对象。除此之外,接口主要用途是被实现类实现。
一个类可以实现一个或多个接口,继承使用extends关键字,实现则使用implements关键字。因为一个类可以实现多个接口,这也是Java为单继承灵活性不足所做的补充。类实现接口的语法格式:
修饰符 class 类名 extends 父类 implements 接口1,接口2...{ 类体部分 }
实现接口与继承父类相似,一样可以获得所实现接口里定义常量属性、抽象方法、内部类和枚举类定义。
让类实现接口需要类定义后增加implements部分,当需要实现多个接口时,多个接口之间以英文逗号(,)隔开。一个类可以继承一个父类,并同时实现多个接口,implements部分必须放在extends部分之后。
一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义抽象类。下面看一个实现接口的类。
import lee.Output; //定义一个Product接口 interface Product { int getProduceTime(); } //让Printer类实现Output和Product接口 public class Printer implements Output , Product { private String[] printData = new String[MAX_CACHE_LINE]; //用以记录当前需打印的作业数 private int dataNum = 0; public void out() { //只要还有作业,继续打印 while(dataNum > 0) { System.out.println("打印机打印:" + printData[0]); //把作业队列整体前移一位,并将剩下的作业数减1 System.arraycopy(printData , 1 , printData, 0, --dataNum); } } public void getData(String msg) { if (dataNum >= MAX_CACHE_LINE) { System.out.println("输出队列已满,添加失败"); } else { //把打印数据添加到队列里,已保存数据的数量加1。 printData[dataNum++] = msg; } } public int getProduceTime() { return 45; } public static void main(String[] args) { //创建一个Printer对象,当成Output使用 Output o = new Printer(); o.getData("轻量级Java EE企业应用实战"); o.getData("疯狂Java讲义"); o.out(); o.getData("疯狂Android讲义"); o.getData("疯狂Ajax讲义"); o.out(); //创建一个Printer对象,当成Product使用 Product p = new Printer(); System.out.println(p.getProduceTime()); //所有接口类型的引用变量都可直接赋给Object类型的变量 Object obj = p; } }
实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只能更大或相等,所以实现类实现接口里的方法时只能使用public访问权限。
接口不能显式继承任何类,但所有接口类型的引用变量都可以直接赋给Object类型的引用变量。所以上面的程序可以把Product类型变量直接赋给Object类型的变量,这是利用向上转型实现,因为编译器知道任何Java对象都必须是Object或其子类的实例。
2.5 接口和抽象类对比
接口和抽象都很像,它们都具有如下特征:
1. 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
2. 接口和抽象类可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
但接口和抽象类之间的差别很大,这种差别主要体现在二者设计目的上,下面具体分析二者的差别。
接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
但接口和抽象类之间的差别非常大,这种差别主要体现在二者的设计目的上,下面具体分析两者的差别。
接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
从某种程度上来看,接口类似于整个系统的总纲,制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。
抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象父类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有跟进一步的完善,这种完善可能有几种不同方式。
除此之外,接口和抽象类在用法上也存在如下区别。
1. 接口里只能包含抽象方法,不包含已经提供实现的方法;抽象类则完全可以包含普通方法。
2. 接口里不能定义静态方法;抽象类里可以定义静态方法。
3. 接口里只能定义静态常量属性,不能定义普通属性;抽象类里则既可以定义普通属性,也可以定义静态常量属性。
4. 接口不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而让其子类调用这些构造器来完成属于抽象类的初始化操作。
5. 接口里不能包含初始化块,但抽象类则完全可以包含初始化块。
6. 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。
2.6 面向接口编程
接口体现的是一种规范和设计分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。
基于这种原则,很多软件架构设计理论都倡导面向接口编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。下面将介绍两种常用场景来示范面向接口编程的优势。
2.6.1 工场模式
有这样的场景,假设程序中有个Computer类需要组合一个输出设备,现在又两个选择:直接让Computer该类组合衣柜Printer属性,或者让Computer组合一个Output属性,采用哪种方式更好呢?
假设让Computer组合一个Printer属性,如果有一天系统需要重构,需要使用BetterPrinter来代替Printer,于是需要打开Computer类源代码进行修改。如果系统中只有一个Computer类组合了Printer属性,那么我们只需要修改这一个Computer类就可以了,但是如果有1000个、10000个类甚至更多的类,那么就需要一个个打开这么多文件进行修改(并不是这么多类都放在一个word文档里面进行批量替换这么简单的)。这种工作量非常之大。为了避免这个问题,我们让Computer组合一个Output属性,将Computer类与Printer类完全分离。Computer对象实际组合的是Printer对象,还是BetterPrinter对象,对Computer而言是屏蔽的,由Output来进行耦合Computer和Output属性
理清一下思路:
假设现在一个应用程序要打印一份文件,那么这个应用程序可以调用windows平台开放的打印程序,而一台计算机上可以连接多个不同类型的打印机,这样打印程序就可以接口的形式来调用具体实现打印功能的类
示例:
下面这个程序可以当做windows平台的打印程序,用于开放给应用程序
package chapter6; public class Computer { private Output out; public Computer(Output out){ this.out = out; } //定义一个模拟字符串输入的方法 public void keyIn(String msg){ out.getData(msg); } public void print(){ out.out(); } }
上面的程序已经将Computer类和Printer类相分离,用Output接口来耦合,也就是使用Output来生产(创建)Printer类对象来供Computer类来使用。不再用Computer来生产。
package chapter6; public class OutputFactory { //制造Printer对象 public Output getOutput(){ return new Printer(); } public static void main(String[] args){ OutputFactory of = new OutputFactory(); Computer c = new Computer(of.getOutput()); c.keyIn("疯狂Java讲义"); c.keyIn("简明德语语法"); c.print(); } } public class Printer implements Output{ private String[] printData = new String[MAX_CACHE_LINE]; //记录当前需要打印的页数 private int dataNum = 0; public void out(){ //只要页数大于0继续打印 while(dataNum > 0){ System.out.println("打印机打印:" + printData[0]); //作业队列整体前移一位,并将剩下的作业减1 System.arraycopy(printData, 1, printData, 0, --dataNum); } } public void getData(String msg){ if(dataNum >= MAX_CACHE_LINE){ System.out.println("输出队列已满,添加失败"); }else{ printData[dataNum++] = msg; } } }
我们可以把上面的Printer代码修改成BetterPrinter,并且在OutputFactory程序中修改制造对象。
2.6.2 命令模式
有这样的场景:某个方法需要完成某个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组的数组元素,但无法确定在遍历数组时候还需要做什么操作,需要在调用该方法时指定具体的处理行为。这个看起来很奇怪,难道调用函数时候,能够把行为作为一个参数?在某些编程语言,如Ruby,这种特性是支持的,在Java里面不支持,但是可以间接做到。
Java是不允许代码块单独存在的,因此可以使用一个Command接口来定义一个方法来封装处理行为。
package chapter6; public interface Command { //定义封装抽象的行为 void process(int[] target); } package chapter6; public class ProcessArray { public void process(int[] target,Command cmd){ cmd.process(target); } }
通过一个Command类,就实现了让ProcessArray类和具体处理行为相分离,程序使用Command接口处理代表处理数组的处理行为。Command接口也没有提供真正的处理,只有等到需要调用ProcessArray对象的process方法时,才真正传入一个Comman对象,才确定对数组的处理行为。
package chapter6; public class PrintCommand implements Command{ public void process(int[] target){ for(int temp:target){ System.out.println("迭代输出的数组是:" + temp); } } } package chapter6; public class AddCommand implements Command{ public void process(int[] target){ int sum = 0; for(int temp:target){ sum += temp; } System.out.println("数组元素总和是:" + sum); } } package chapter6; public class TestCommand { public static void main(String[] args){ ProcessArray pa = new ProcessArray(); int[] target = {3,4,9}; //第一次处理数组 pa.process(target, new PrintCommand()); System.out.println("---------------"); //第二次处理数组 pa.process(target, new AddCommand()); } }
3 内部类
大部分时候,我们把类定义成一个独立的程序单元。在某些情况下,我们把一个类放在另一个类的内部定义,这个定义在其他类内部的类被称为内部类(也被称为嵌套类),包含内部类的类也被称为外部类(有的地方也叫宿主类)。内部类有如下作用。
1.内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。假设需要创建一个Cow类,Cow类需要组合一个CowLeg属性,CowLeg类只有在Cow类里才有效,离开Cow类之后没有任何意义。这种情况下就可以把CowLeg类定义成Cow的内部类,不允许其他类访问CowLeg
2.内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问,但外部类不能访问内部类的实现细节,例如内部类的属性。
3.匿名内部类适合用于创建那些仅需要一次使用的类。对于前面介绍的命令模式,当需要传入一个Command对象时,重新专门定义PrintCommand和AddCommand两个实现类可能没有太大的意义,因为这两个实现类可能仅需要使用一次。这种情况下,匿名内部类使用更方便。
3.1 非静态内部类
1 非静态内部类 2 public class OuterClass{ 3 //此处定义内部类 4 }
大部分时候,内部类被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与属性、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。
成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。
1 package chapter6; 2 3 public class Cow { 4 private double weight; 5 //外部类的两个重载构造器 6 public Cow(){ 7 8 } 9 public Cow(double weight){ 10 this.weight = weight; 11 } 12 //定义一个非静态内部类 13 private class CowLeg{ 14 //非静态内部类的两个属性 15 private double length; 16 private String color; 17 //非静态内部类的两个构造器 18 public CowLeg(){ 19 20 } 21 public CowLeg(double length,String color){ 22 this.length = length; 23 this.color = color; 24 } 25 public void setLength(double length){ 26 this.length = length; 27 } 28 public double getLength(){ 29 return length; 30 } 31 public void setColor(String color){ 32 33 } 34 public String getColor(){ 35 return color; 36 } 37 //非静态内部类的实例方法 38 public void info(){ 39 System.out.println("当前牛腿的颜色是: " + color + " 高:" + length); 40 //直接访问外部类属性 41 System.out.println("本牛腿所在的奶牛体重:" + weight); 42 } 43 } 44 //用内部类创建对象 45 public void test(){ 46 CowLeg cl = new CowLeg(1.1,"黑白相间"); 47 cl.info(); 48 } 49 public static void main(String[] args){ 50 Cow c = new Cow(380); 51 c.test(); 52 } 53 } 54 输出结果: 55 当前牛腿的颜色是: 黑白相间 高:1.1 56 本牛腿所在的奶牛体重:380.0
从上面的结果可以看出:
1.内部类和类的属性一样,有多种作用域,当定义成private时,访问级别是类内部,内部类可以访问外部类的属性、方法,外部类里的方法也可以访问内部类的属性、方法
2.内部类的使用和普通类使用没有区别。
3.编译程序可以看出生成两个class文件,一个是Cow.class一个是Cow$CowLeg.class,前者是外部类的Cow的class文件,后者是内部类的class文件。
当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在该名字的局部变量,就使用该变量;如果不存在,则到该方法所在的内部类中查找是否存在该名字的属性,如果存在则使用该属性;如果不存在,则到该内部类所在的外部类中查找是否存在该名字的属性,如果存在则使用该属性。如果依然不存在,系统将出现编译错误:提示找不到该变量。
因此,如果外部类属性、内部类属性与内部类里方法的局部变量同名,则可通过使用this、外部类类名.this作为限定来区分。
1 package chapter6; 2 3 public class DiscernVariable { 4 private String prop = "外部类属性"; 5 private class InnerClass{ 6 private String prop = "内部类属性"; 7 public void info(){ 8 String prop = "局部变量属性"; 9 //通过外部类.this.外部类属性名访问外部类属性 10 System.out.println("外部类属性值:" + DiscernVariable.this.prop); 11 //通过this.内部属性名访问内部属性 12 System.out.println("内部类属性值:" + this.prop); 13 //访问方法内的局部变量值 14 System.out.println("局部属性值:" + prop); 15 } 16 } 17 public void test(){ 18 InnerClass in = new InnerClass(); 19 in.info(); 20 } 21 public static void main(String[] args){ 22 new DiscernVariable().test(); 23 } 24 }
非静态内部类的成员可以访问外部类的private成员,但反过来就不成立了。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问非静态内部类成员,则必须显式创建非静态内部类对象来调用访问其实例成员。
1 public class Outer 2 { 3 private int outProp = 9; 4 class Inner 5 { 6 private int inProp = 5; 7 public void acessOuterProp() 8 { 9 //非静态内部类可以直接访问外部类的成员 10 System.out.println("外部类的outProp值:" 11 + outProp); 12 } 13 } 14 public void accessInnerProp() 15 { 16 //外部类不能直接访问非静态内部类的实例Field, 17 //下面代码出现编译错误 18 //System.out.println("内部类的inProp值:" + inProp); 19 //如需访问内部类的实例Field,必须显式创建内部类对象 20 System.out.println("内部类的inProp值:" 21 + new Inner().inProp); 22 } 23 public static void main(String[] args) 24 { 25 //执行下面代码,只创建了外部类对象,还未创建内部类对象 26 Outer out = new Outer(); //① 27 out.accessInnerProp(); 28 } 29 }
3.1.1 非静态内部类对象和外部类对象的关系
非静态内部类对象必须寄存在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄存其中。简单的说,如果存在一个非静态内部类对象,则一定存在一个被它寄存的外部类对象。但外部类对象存在时,外部类对象里不一定寄存了非静态内部类对象。因此外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在。而非静态内部类对象访问外部类成员时,外部类对象一定是存在的。
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量,创建实例等。总之,不能在外部类的静态成员中直接使用非静态内部类。
1 public class InnerNoStatic 2 { 3 private class InnerClass 4 { 5 /* 6 下面三个静态声明都将引发如下编译错误: 7 非静态内部类不能有静态声明 8 */ 9 static 10 { 11 System.out.println("=========="); 12 } 13 private static int inProp; 14 private static void test(){} 15 } 16 }
3.2 静态内部类
如果使用static来修饰一个内部类,则这个内部类变成是外部类相关的,属于整个外部类,而不是单独属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方称为静态内部类。
static关键字的作用是把类的成员变成类相关,而不是实例相关,即static修饰成员是属于整个类,而不是属于单个对象。外部类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以讲内部类变成外部类相关,而不是外部类实例相关,因此static关键字不可修饰外部类,可以修饰内部类。
静态内部类可以包含静态成员,也可以包含非静态成员。静态内部类不能访问外部类实例成员,只能访问外部类成员。即使静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。
外部类不能直接访问静态内部类成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类的对象作为调用者来访问静态内部类的实例成员。
1 package chapter6; 2 3 public class AccessStaticInnerClass { 4 static class InnerClass{ 5 private static String prop1 = "Hello"; 6 private int prop2 = 3; 7 } 8 public void AccessStaticInnerClass(){ 9 //直接访问报错误 10 //System.out.println(prop1); 11 //通过内部静态类名访问 12 System.out.println(InnerClass.prop1); 13 //通过创建实例来访问内部类属性 14 System.out.println(new InnerClass().prop2); 15 } 16 17 }
除此之外,Java还允许在接口里定义内部类,接口里定义的内部类默认使用public static修饰,也就是说接口内部类只能是静态内部类。
如果为接口内部类指定访问控制符,则只能指定public访问控制符。如果定义接口内部类时省略访问控制符,则该内部类默认是public访问控制权限。
接口里也可以定义内部接口,但这种做法用处不大,接口里的内部接口是接口的成员,接口的作用是定义一个公共规范,如果定义成一个内部接口,那么意义不大。
3.3 使用内部类
定义类的主要作用就是定义变量、创建实例和作为父类被继承。定义内部类的主要作用也如此。但使用内部类定义变量和创建实例则与外部类存在一些小小的差异。下面分三种情况讨论内部类的用法。
3.3.1在外部类内部使用内部类
在前面程序中可以看出,在外部类的内部使用类时,与平常使用普通类没有太大区别。一样可以直接通过内部类类名来定义变量,通过new 调用内部类构造器来创建实例。
唯一存在的一个区别是:不要在外部类的静态成员(静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。
在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。
3.3.2 在外部类以外使用非静态内部类
如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。对于使用其他访问控制符修饰的内部类,则能在访问控制符对应访问权限内使用。
1.省略访问控制符的内部类,只能被与外部类处于同一个包中其他类所访问。
2.使用protected修饰的内部类:可被与外部类处于同一个包中其他类和外部类的子类访问
3.使用public修饰的内部类:可在任何地方被访问。
在外部类以外地方来使用内部类的语法是:
OuterClass.InnerClass varName
也就是要用相对完整的类名,如果在别的包外,还需要包名。
因为非静态内部类的对象必须寄存在外部类的对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类实例的语法如下:
OuterInstance.new InnerConstructor()
也就是说,在外部类以外的地方创建非静态内部类实例必须使用外部类实例和new来调用非静态内部类构造器。
1 class Out 2 { 3 //定义一个内部类,不使用访问控制符, 4 //即只有同一个包中其他类可访问该内部类 5 class In 6 { 7 public In(String msg) 8 { 9 System.out.println(msg); 10 } 11 } 12 } 13 public class CreateInnerInstance 14 { 15 public static void main(String[] args) 16 { 17 Out.In in = new Out().new In("测试信息"); 18 /* 19 上面代码可改为如下三行代码: 20 使用OutterClass.InnerClass的形式定义内部类变量 21 Out.In in; 22 创建外部类实例,非静态内部类实例将寄存在该实例中 23 Out out = new Out(); 24 通过外部类实例和new来调用内部类构造器创建非静态内部类实例 25 in = out.new In("测试信息"); 26 */ 27 } 28 }
如果需要在外部类以外的地方创建非静态内部类的子类,尤其需要注意上面的规则:非静态内部类的构造器必须通过其外部类对象来调用。
我们知道:当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类的构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类的对象。下面程序定义了一个子类继承了Out类的非静态内部类In
1 public class SubClass extends Out.In{ 2 //显式定义SubClass的构造器 3 public SubClass(Out out){ 4 //通过传入Out对象显式调用In的构造器 5 out.super("hello"); 6 } 7 }
上面的代码可以看出如果要创建一个SubClass对象,必须先创建一个Out对象。因为SubClass是非静态内部类In的子类,非静态内部类In对象里必须有一个对Out对象的引用。其子类SubClass对象里也应该有一个Out对象的引用。
非静态内部类In对象和SubClass对象都必须保留有指向Outer对象的引用,区别是创建两种对象时传入Out对象方式不同:当创建非静态内部类In类的对象时,必须通过Outer对象来调用new 关键字;当创建SubClass类的对象时,必须将Outer对象作为参数传给SubClass的构造器。
非静态内部类的子类不一定是内部类,也可以是顶层类。但非静态内部类的子类实例一样需要保留一个引用,该引用就是指向子类的父类所在的外部类的对象,也就是说,如果有一个内部类的子类对象存在,一定存在与之对应外部类的对象。
3.3.3 在外部类以外使用静态内部类
静态内部类是外部类相关的,因此创建内部类对象时无需创建外部类的对象。
语法是:
1 new OuterClass.InnerConstructor() 2 class StaticOut{ 3 //定义一个静态内部类,不使用访问控制符, 4 //即同一个包中其他类可访问该内部类 5 static class StaticIn{ 6 public StaticIn(){ 7 System.out.println("静态内部类的构造器"); 8 } 9 } 10 } 11 public class CreateStaticInnerInstance{ 12 public static void main(String[] args){ 13 StaticOut.StaticIn in = new StaticOut.StaticIn(); 14 /* 15 上面代码可改为如下两行代码: 16 使用OutterClass.InnerClass的形式定义内部类变量 17 StaticOut.StaticIn in; 18 通过new来调用内部类构造器创建静态内部类实例 19 in = new StaticOut.StaticIn(); 20 */ 21 } 22 }
因为调用静态内部类的构造器无需使用外部类对象,所以创建静态内部类的子类比较简单,下面定义静态内部类StaticIn定义了一个空的子类
public class StaticSubClass extends StaticOut.StaticIn{}
3.4 局部内部类
如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。因此,局部内部类不能在外部类以外的地方使用,那么局部内部类也无需使用访问控制符和static修饰符修饰。
对局部成员而言,不管是局部变量还是局部内部类,它们的上一级程序单元是方法,而不是类,使用static修饰它们没有任何意义。因此,所有局部成员都不能使用static修饰。不仅如此,因为局部成员的作用域是所在方法,其他程序单元永远也不可能访问另一个方法中的局部成员,所以局部成员都不能使用访问控制符修饰。
1 package chapter6; 2 3 public class LocalInnerClass { 4 public static void main(String[] args){ 5 //定义局部内部类 6 class InnerBase{ 7 int a; 8 } 9 //定义局部内部类的子类 10 class SubInnerBaseClass extends InnerBase{ 11 int b; 12 } 13 //局部类对象 14 SubInnerBaseClass sb = new SubInnerBaseClass(); 15 sb.a = 3; 16 sb.b = 4; 17 System.out.println(sb.a); 18 System.out.println(sb.b); 19 } 20 }
编译上面程序,看到生成三个class文件LocalInnerClass$1SubInnerBaseClass.class,LocalInnerClass$1InnerBase.class,LocalInnerClass.class,局部内部类的class文件综述遵循如下命名格式:OuterClass$NInnerClass.class,注意到局部内部类的class文件的文件名比成员内部类的class文件的文件名多了一个数字,这是因为同一个类里不可能有两个同名的成员内部类,而同一个类里面可能有两个以上同名的局部内部类。所以Java为局部内部类的class文件名增加一个数字用于区分。
3.5 匿名内部类
匿名内部类适合一次性创建使用的类,例如命令模式的对象,创建匿名内部类时会立即创建该类的一个实例,这个类的定义立即消失,匿名内部类不能重复使用。
语法格式:
1 new 父类名称|接口{ 2 3 4 }
从上面的定义可以看出匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类或实现一个接口
关于匿名内部类还有如下两条规则:
1.匿名内部类不能是抽象类,因为系统在创建匿名内部类的时候,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
2.匿名内部类不能定义构造器,因为匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义实例初始化块,通过实例初始化块来完成构造器需要完成的事情。
最常用的创建匿名内部类的方式是需要创建某个接口类型的对象,如下所示:
package chapter6; interface Product{ public double getPrice(); public String getName(); } public class TestAnonymous { public void test(Product p){ System.out.println("购买了一个名为" + p.getName() + " 价格为:" + p.getPrice()); } public static void main(String[] args){ TestAnonymous ta = new TestAnonymous(); //调用test方法时候,需要传递一个Product类对象作为参数 ta.test(new Product(){ public double getPrice(){ return 6.3; } public String getName(){ return "南瓜"; } }); } }
上面程序中的TestAnonymous类定义了一个test方法,该方法需要一个Product对象作为参数,但Product只是一个接口,无法直接创建对象,因此此处考虑创建一个Product接口实现类的对象传入该方法——如果这个Product接口实现类需要重复使用,则应该讲该实现类定义成一个独立类;如果这个Product接口实现类只需一次使用,则可以采用上面程序中的方式,定义一个匿名内部类。
匿名内部类无须class关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象。
由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或者接口里面包含的所有抽象方法。
上面创建Product实现类对象的代码,可以拆分成如下代码:
1 class AnonymousProduct implements Product{ 2 public double getPrice(){ 3 return 6.3; 4 } 5 public String getName(){ 6 return "南瓜"; 7 } 8 ta.test(new AnonymousProduct());
显然使用内部类是更加简洁一点。
当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口名后的括号里不能传入参数值。
但如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,此处的相似指的是拥有相同的形参列表。
1 abstract class Device{ 2 private String name; 3 public abstract double getPrice(); 4 public Device(){} 5 public Device(String name){ 6 this.name = name; 7 } 8 public void setName(String name){ 9 this.name = name; 10 } 11 public String getName(){ 12 return this.name; 13 } 14 } 15 public class AnonymousInner{ 16 public void test(Device d){ 17 System.out.println("购买了一个" + d.getName() + ",花掉了" + d.getPrice()); 18 } 19 public static void main(String[] args){ 20 AnonymousInner ai = new AnonymousInner(); 21 //调用有参数的构造器创建Device匿名实现类的对象 22 ai.test(new Device("电子示波器"){ 23 public double getPrice(){ 24 return 67.8; 25 } 26 }); 27 //调用无参数的构造器创建Device匿名实现类的对象 28 Device d = new Device(){ 29 //初始化块{ 30 System.out.println("匿名内部类的初始化块..."); 31 } 32 //实现抽象方法 33 public double getPrice(){ 34 return 56.2; 35 } 36 //重写父类的实例方法 37 public String getName(){ 38 return "键盘"; 39 } 40 }; 41 ai.test(d); 42 } 43 }
上面程序创建了一个抽象父类Device,这个抽象父类里包含两个构造器:一个无参数一个有参数。当创建以Device为父类的匿名内部类时,既可以传入参数,也可以不传入参数。
当创建匿名内部类时,必须实现接口或抽象方法里的所有抽象方法。如果有需要,也可以重写父类中的普通方法。
如果匿名内部类需要访问外部类的局部变量,则必须使用final修饰符来修饰外部类的局部变量,否则系统将报错。
1 interface A{ 2 void test(); 3 } 4 public class TestA{ 5 public static void main(String[] args){ 6 int age = 0; 7 A a = new A(){ 8 public void test(){ 9 //下面语句将提示错误:匿名内部类内访问局部变量必须使用final修饰 10 System.out.println(age); 11 } 12 }; 13 } 14 }
注意:如果没有A a = new A()后面的大括号进行初始化,只有A a = new A();是不对的,因为接口没有构造器,无法进行实例化
3.6 闭包和回调
闭包就是一种内部类,用内部类来实现外部接口,并且可以直接调用外部类的private成员(也就是回调)。
Java并不能显式支持闭包,但对于非晶态内部类而言,它不仅记录了其外部类的详细信息,还保留了一个创建非静态内部类对象的引用,并且可以直接调用外部类的private成员,因此可以把非静态内部类当成面向对象领域的闭包。
通过这种仿闭包的非静态内部类,可以很方便地实现回调功能,回调就是某个方法一旦获得了内部类对象引用后,就可以在合适时候反过来调用外部类实例的方法。
下面的Teachable和Programmer基类都提供了work方法,这两个方法的签名一样,但是方法功能不同。
1 package chapter6; 2 3 interface Teachable { 4 public void work(); 5 } 6 package chapter6; 7 8 public class Programmer { 9 protected String name; 10 //无参构造器 11 public Programmer(){ 12 13 } 14 //有参数构造器 15 public Programmer(String name){ 16 this.name = name; 17 } 18 //省略了getter和setter方法 19 public void work(){ 20 System.out.println(name + "在灯下认真敲键盘"); 21 } 22 }
假设现在有一个人,既是程序员,也是一个教师,也就是需要定义一个特殊的类,既需要实现Teachable接口,也需要继承Programmer父类,表面上看起来没有任何问题,问题是Teachable接口和Programmer父类里包含了相同的work方法,如果按照下面代码来定义一个特殊的TeachableProgrammer类,是有问题的
1 package chapter6; 2 3 public class TeachableProgrammer extends Programmer implements Teachable{ 4 public void work(){ 5 System.out.println(super.name + "教师在课堂上讲解"); 6 } 7 }
显然上面的TeachableProgrammer类只有一个work方法,这个work方法只能进行教学,不再可以进行编程,但实际需要两者技能都要具备。
可以用一个内部类来实现这个功能
1 package chapter6; 2 3 public class TeachableProgrammer extends Programmer{ 4 public TeachableProgrammer(){ 5 6 } 7 public TeachableProgrammer(String name){ 8 super.name = name; 9 } 10 public void teach(){ 11 System.out.println(getName() + "教师在课堂上讲解"); 12 } 13 private class Closure implements Teachable{ 14 //非静态内部类回调外部类的work方法 15 public void work(){ 16 teach(); 17 } 18 } 19 //返回一个非静态内部类引用,使得外部类允许非静态内部类引用回调外部类的方法 20 public Teachable getCallBackReference(){ 21 return new Closure(); 22 } 23 }
上面的TeachableProgrammer至少Programmer类的子类,它可以直接调用Programmer基类的work方法,该类也包含教学teach方法,单子合格方法与Teachable接口没有任何关系,TeachableProgrammer也不能当场Teachable使用,此时创建了一个内部类,实现了Teachable接口,并实现了教学的work方法。但这种实现是通过回调TeachableProgrammer类的teach方法实现的。如果需要让TeachableProgrammer对象进行教学,只需要调用Closure内部类(它是Teachable接口的实现类)对象的work方法即可。
TeachableProgrammer类提供了一个获取内部类对象的方法:该方法无需返回Closure类型,只需要返回所实现接口Teachable类型即可,因为它只需要当初一个Teachable对象使用即可。
1 public class TestTeachableProgrammer 2 { 3 public static void main(String[] args) 4 { 5 TeachableProgrammer tp = new TeachableProgrammer("李刚"); 6 //直接调用TeachableProgrammer类从Programmer类继承到的work方法 7 tp.work(); 8 //表面上调用的是Closure的work方法,实际上是回调TeachableProgrammer的teach方法 9 tp.getCallbackReference().work(); 10 } 11 }
4 枚举类
在某些情况下,一个类的对象是有限而且固定的,例如季节类,它只有四个对象。这种实例有限而且固定的类,在Java里被称为枚举类。
手动实现枚举类
如果需要手动实现枚举类,可以采用如下设计方式:
1.通过private将构造器隐藏起来
2.把这个类的所有可能实例都使用public static final属性来保存
3.如果有必要,可以提供一些静态方法,允许其他程序根据特定参数来获取与之匹配的实例。
1 public class Season{ 2 //把Season类定义成不可变的,将其属性也定义成final 3 private final String name; 4 private final String desc; 5 public static final Season SPRING = new Season("春天" , "趁春踏青"); 6 public static final Season SUMMER = new Season("夏天" , "夏日炎炎"); 7 public static final Season FALL = new Season("秋天" , "秋高气爽"); 8 public static final Season WINTER = new Season("冬天" , "围炉赏雪"); 9 10 public static Season getSeaon(int seasonNum){ 11 switch(seasonNum){ 12 case 1 : 13 return SPRING; 14 case 2 : 15 return SUMMER; 16 case 3 : 17 return FALL; 18 case 4 : 19 return WINTER; 20 default : 21 return null; 22 } 23 } 24 25 //将构造器定义成private访问权限 26 private Season(String name , String desc){ 27 this.name = name; 28 this.desc = desc; 29 } 30 //只为name和desc属性提供getter方法 31 public String getName(){ 32 return this.name; 33 } 34 public String getDesc(){ 35 return this.desc; 36 } 37 }
上面的方式有些麻烦,J2SE1.5新增了一个enum关键字,用以定义枚举类。枚举类是特俗的类。它可以有自己的方法和属性,可以实现一个或者多个接口,也可以定义自己的构造器。一个Java源文件最多只能定义一个public访问权限的枚举类,且该Java源文件必须和该枚举类的类名相同。
但枚举类终究不是普通类,有自己的如下特点:
1.枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类,其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口
2.枚举类的构造器只能使用private访问控制符,如果省略了其构造器的访问控制符,则默认使用private修饰,如果强制指定,则只能使用private修饰符
3.枚举类的所有实例都必须在枚举类中显式列出,否则这个枚举类将永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。
4.所有枚举类都提供了一个values方法,该方法可以很方便的遍历所有的枚举值。
1 package chapter6; 2 3 public enum SeasonEnum { 4 SPRING,SUMMER,FALL,WINTER; 5 public static void main(String[] args){ 6 System.out.println(SeasonEnum.SPRING); 7 8 } 9 }
编译上面的程序可以生成一个class文件,enum关键字和class、interface关键字的作用大致类似。
定义枚举类时,需要显式列出所有枚举值,如上面的 SPRING等,枚举值之间用逗号,隔开,枚举值列举结束后以英文分号作为结束。这些枚举值是枚举类的实例。
1 package chapter6; 2 3 public class TestEnum { 4 public void judge(SeasonEnum s){ 5 //swich分支语句 6 switch(s){ 7 case SPRING: 8 System.out.println("面朝大海,春暖花开~"); 9 break; 10 case SUMMER: 11 System.out.println("像夏花一样绚烂"); 12 break; 13 case FALL: 14 System.out.println("自古逢秋悲寂寥,我言秋日胜春朝"); 15 break; 16 case WINTER: 17 System.out.println("你就像那冬天里的一把火"); 18 break; 19 } 20 } 21 public static void main(String[] args){ 22 //列出枚举类的所有实例 23 for(SeasonEnum s:SeasonEnum.values()){ 24 System.out.println(s); 25 } 26 //直接访问单个实例 27 new TestEnum().judge(SeasonEnum.SPRING); 28 } 29 } 30 输出: 31 SPRING 32 SUMMER 33 FALL 34 WINTER 35 面朝大海,春暖花开~
4.1 枚举类的属性、方法和构造器
枚举类也是一种类,只是它是一种比较特殊的类,因此它一样可以使用属性和方法。
1 public enum Gender{ 2 MALE,FEMALE; 3 public String name; 4 5 } 6 上面的Gender枚举类里定义了一个name属性,并且将它定义成一个public访问权限的属性,下面使用该枚举类 7 public class TestGender{ 8 public static void main(String[] args){ 9 Gender g = Enum.valueOf(Gender.class , "FEMALE"); 10 g.name = "女"; 11 System.out.println(g + "代表:" + g.name); 12 } 13 }
注意:Enum类的实例生成不是通过new的,而是通过方法valueOf来解决。
正如前面提到的,Java应该把所有类设计成良好封装的类,所以不应该允许直接访问Gender类的name属性,而应该通过方法来控制访问。
4.2 实现接口的枚举类
枚举类可以实现一个或多个接口。与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口,也需要实现该接口所包含的方法。
1 public interface GenderDesc{ 2 void info(); 3 }
下面的类实现了这个接口
1 public String getName(){ 2 return this.name; 3 } 4 /* 5 public void info(){ 6 System.out.println("这是一个用于用于定义性别属性的枚举类"); 7 }
如果由枚举类来实现接口里的方法,则每个枚举值在调用该方法时,都有相同的行为方式(因为方法体完全一样)。如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法,每个枚举值提供不同的实现方式,从而让不同枚举值调用该方法时具有不同的行为方式,下面的Gender枚举类中,不同枚举值对info方法的实现则各不相同。
1 public enum Gender implements GenderDesc{ 2 //此处的枚举值必须调用对应构造器来创建 3 MALE("男"){ 4 public void info(){ 5 System.out.println("这个枚举值代表男性"); 6 } 7 }, 8 FEMALE("女"){ 9 public void info(){ 10 System.out.println("这个枚举值代表女性"); 11 } 12 }; 13 private String name; 14 //枚举类的构造器只能使用private修饰 15 private Gender(String name){ 16 this.name = name; 17 } 18 }
上面的MALE和FEMALE后面跟了一个花括号,花括号部分实际上是一个类体部分,这种情况下,当创建枚举值时,并不是直接创建了Gender枚举类的实例,而是相当于创建Gender的匿名子类的实例。也就是一个匿名内部类的类体部分,所以这个部分的代码语法与前面介绍的匿名内部类语法大致相似,依然是枚举类的匿名内部子类。
编译上面的程序,可以看到生成了Gender.class、Gender$1.class和Gender$2.class三个文件。这也就是说MALE和FEMALE实际上是Gender匿名子类的实例,而不是Gender类的实例。
4.3 包含抽象方法的枚举类
枚举类的枚举值就是实例值
假设有一个Operation枚举类,它的四个枚举值PLUS,MINUS,TIMES,DIVIDE分别代表加减乘除,为此定义枚举类如下
1 package chapter6; 2 3 public enum Operation { 4 PLUS,MINUS,TIMES,DIVIDE; 5 double eval (double x,double y){ 6 switch(this){ 7 case PLUS: 8 return x + y; 9 case MINUS: 10 return x - y; 11 case TIMES: 12 return x*y; 13 case DIVIDE: 14 return x/y; 15 default: 16 return 0; 17 } 18 } 19 public static void main(String[] args){ 20 System.out.println(Operation.PLUS.eval(2, 3)); 21 System.out.println(Operation.MINUS.eval(2, 3)); 22 System.out.println(Operation.TIMES.eval(2, 3)); 23 System.out.println(Operation.DIVIDE.eval(2, 3)); 24 } 25 }
上面的枚举类可以实现四个方法,this代表四个枚举类的实例值。这四个值是确定的,不能有其他值。实际上Operation类的四个值对eval方法各有不同的实现。为此可以采用前面MALE/FEMALE的方法,让它们分别为四个枚举值提供eval实现,然后在Operation类中定义一个eval的抽象方法。
package chapter6; public enum Operation2 { PLUS{ public double eval(double x,double y){ return x + y; } }, MINUS{ public double eval(double x,double y){ return x - y; } }, TIMES{ public double eval(double x,double y){ return x*y; } }, DIVIDE{ public double eval(double x,double y){ return x/y; } }; //提供抽象方法,但是是放在下面的 public abstract double eval(double x,double y); public static void main(String[] args){ System.out.println(Operation2.PLUS.eval(2, 3)); System.out.println(Operation2.MINUS.eval(2, 3)); System.out.println(Operation2.TIMES.eval(2, 3)); System.out.println(Operation2.DIVIDE.eval(2, 3)); } } 输出: 5.0 -1.0 6.0 0.6666666666666666
编译上面的程序会生成5个class文件,其实Operation2对应一个class文件,它的四个匿名内部子类分别各对应一个class文件。
枚举类里定义抽象方法时无需显式使用abstract关键字将枚举类定义成抽象类,但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。
5 对象与垃圾回收
垃圾回收是Java语言的重要功能,当程序创建对象、数组等引用类型实体时,系统都会在堆内存为之分配一块内存区,对象就保存在这块内存区中,当这块内存不再被引用变量引用时,这块内存就变成垃圾,等待垃圾回收机制进行回收。垃圾回收机制具有如下特征:
1.垃圾回收机制只负责回收堆内存中对象,不会回收任何物理资源(例如数据库连接、网络IO等资源)
2.程序无法精确控制垃圾回收的运行,垃圾回收会在合适时候运行。当对象永久性地失去引用后,系统就会在合适时候回收它所占的内存。
3.垃圾回收机制回收任何对象之前,总会先调用它的finalize方法,该方法可能使该对象重新复活(让一个引用该变量重新引用该对象),从而导致垃圾回收机制取消回收。
5.1 对象在内存中的状态
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:
1.激活状态:当一个对象被创建后,有一个以上的引用变量引用它,则这个对象在程序中处于激活状态,程序可通过引用变量来调用该对象的属性和方法。
2.去活状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了去活状态。在这个状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有去活状态对象的finalize方法进行资源清理,如果系统在调用finalize方法重写让一个引用变量引用该对象,则这个对象会再次变为激活状态;否则该对象将进入死亡状态
3.死亡状态:当对象与所有变量的关联都被切断,且系统已经调用所有对象的finalize方法,依然没有使该对象变成激活状态,那这个对象将永久性地失去引用,最后变成死亡状态。只有当一个对象处于死亡状态时,系统才会真正回收该对象所占有的资源。
1 下面的程序说明了上面的原理: 2 package chapter6; 3 4 public class StatusTransfer { 5 public static void test(){ 6 String a = new String("Englis");① 7 a = new String("Deutsch");② 8 } 9 public static void main(String[] args){ 10 test();③ 11 } 12 13 }
当程序执行test方法①代码时,代码定义了一个a变量,并让该变量指向字符串English,代码执行结束后,字符串对象English处于激活状态。当程序执行了test方法的②代码后,代码再次定义了Deutsch对象,并让a变量指向这个对象,此时English处于去活状态,而Deutsch处于激活状态。
一个对象可以被一个方法局部变量所引用,也可以被其他类的类属性引用,或被其他对象的实例属性引用。当被类属性引用时,只有类被销毁,对象才会进入去活状态,当被其他对象的实例属性引用时,只有该对象被销毁,该对象才会进入去活状态。
5.2 强制垃圾回收
程序无法精确控制Java垃圾回收的时机,但我们依然可以强制系统进行垃圾回收——只是这种机制是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时候,强制垃圾回收会有效果,强制垃圾回收有如下两个方法。
1.调用System类的gc()静态方法:System.gc();
2.调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()
1 public class TestGc{ 2 private double height; 3 public static void main(String[] args){ 4 for (int i = 0 ; i < 4; i++){ 5 new TestGc(); 6 //System.gc(); 7 Runtime.getRuntime().gc(); 8 } 9 } 10 public void finalize(){ 11 System.out.println("系统正在清理TestGc对象的资源..."); 12 } 13 }
5.1 finalize方法
在垃圾回收机制回收某个对象所占用的内存之前,通常要求程序调用适当的方法来清理资源,在没有明确指定资源清理的情况下,Java提供了默认机制来清理该对象的资源,这个方法是finalize,它是Object类的实例方法,方法原先为:
protected void finalize() throws Throwable
当finalize()方法返回之后,对象消失,垃圾回收机制开始执行。方法原型中的throws Throwable表示可以抛出任何类型的异常。
任何Java类都可以覆盖Object类的finalize方法,在该方法中清理该对象占用的资源。如果程序终止前始终没有进行垃圾回收,则不会调用失去引用对象的finalize方法来清理资源。垃圾回收机制何时调用对象的finalize方法是完全透明的,只有当程序认为需要更多额外内存时,垃圾回收机制才会进行垃圾回收。
finalize方法有如下四个特点:
1.永远不要主动调用某个对象的finalize方法,该方法应交给垃圾回收机制调用。
2.finalize方法何时被调用,是否被调用具有不确定性。不要把finalize方法当成一定会被执行的方法
3.当JVM执行去活对象的finalize方法时,可能使该对象或系统中其他对象重新变成激活状态
4.当JVM执行finalize方法时出现了异常,垃圾回收机制不会报告异常,程序继续执行。
1 public class TestFinalize{ 2 private static TestFinalize tf = null; 3 public void info(){ 4 System.out.println("测试资源清理的finalize方法"); 5 } 6 public static void main(String[] args) throws Exception{ 7 //创建TestFinalize对象立即进入去活状态 8 new TestFinalize(); 9 //通知系统进行资源回收 10 System.gc(); 11 System.runFinalization(); 12 //Thread.sleep(2000); 13 tf.info(); 14 } 15 public void finalize(){ 16 //让tf引用到试图回收的去活对象,即去活对象重新变成激活 17 tf = this; 18 } 19 }
上面程序中定义了一个TestFinalize类,重写了finalize方法,该方法使一个tf引用变量引用的对象从去活对象重新变成激活状态。
除此之外,System和Runtime类里都提供了一个runFinalization方法,可以强制垃圾回收机制调用系统去活对象的finalize方法。
5.2 对象的软、弱和虚引用
对大部分对象而言,程序里会有一个引用变量引用该对象,这种引用方式是最常见的引用方式。除此之外,java.lang.ref包下提供了三个类:SoftReference、PhantomReference和WeakReference,它们分别代表了系统对对象的三种引用方式:软引用、虚引用和弱引用。因此Java语言对对象的引用有如下四种。
强引用
这是Java程序中最常见的引用方式,程序创建一个对象,并把这个对象赋给一个引用变量。程序通过该引用变量来操作实际的对象,前面介绍的对象和数组都是采用了这种强引用的方式。当一个对象被一个或一个以上的引用变量所引用时,它处于激活状态,不可能被系统垃圾回收机制回收。
软引用
软引用需要通过SoftReference类来实现,当一个对象只具有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统将会回收它。软引用通常用于对内存敏感的程序中。
弱引用
弱引用通过WeakReference类实现,弱引用和软引用很像,但弱引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。
虚引用
虚引用通过PhantomReference类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那它和没有引用的效果大致相同。虚引用主要用于跟踪对象呗垃圾回收的状态,虚引用不能单独使用,必须和引用队列ReferenceQueue联合使用。
软、弱、虚引用三种都有一个get方法
1 public class TestReference{ 2 public static void main(String[] args) throws Exception{ 3 //创建一个字符串对象 4 String str = new String("Struts2权威指南"); 5 //创建一个弱引用,让此弱引用引用到"Struts2权威指南"字符串 6 WeakReference wr = new WeakReference(str); 7 //切断str引用和"Struts2权威指南"字符串之间的引用 8 str = null; 9 //取出弱引用所引用的对象 10 System.out.println(wr.get()); 11 //强制垃圾回收 12 System.gc(); 13 System.runFinalization(); 14 //再次取出弱引用所引用的对象 15 System.out.println(wr.get()); 16 } 17 }
6 jar命令
为了用 JAR 文件执行基本的任务,要使用作为Java Development Kit 的一部分提供的 Java Archive Tool ( jar 工具)。用 jar 命令调用 jar 工具。表 1 显示了一些常见的应用:
常见的 jar 工具用法
功能
命令
用一个单独的文件创建一个 JAR 文件 jar cf jar-file input-file...
用一个目录创建一个 JAR 文件 jar cf jar-file dir-name
创建一个未压缩的 JAR 文件 jar cf0 jar-file dir-name
更新一个 JAR 文件 jar uf jar-file input-file...
查看一个 JAR 文件的内容 jar tf jar-file
提取一个 JAR 文件的内容 jar xf jar-file
从一个 JAR 文件中提取特定的文件 jar xf jar-file archived-file...
运行一个打包为可执行 JAR 文件的应用程序 java -jar app.jar
jar包就是zip包,可以用一些windows自带的工具如winrar等解压缩文件进行处理。使用WinRAR工具创建JAR包时候,因为工具本身不会自动添加清单文件,所以需要手动添加清单文件,即需要手动建立META-INF路径,并在该路径下建立MANIFEST.MF文件,该文件至少需要如下两行:
Manifest-Version:1.0
Created-By:1.6.0_03(Sun Microsystem Inc.)
除此之外,Java还能生成两种压缩包:WAR包和EAR,WAR文件是Web Archive File,对应一个Web应用文档,EAR是Enterprise Archive File对应企业应用文档,有Web和EJB两个部分组成。WAR、EAR和JAR包完全一样,至少改变了文件后缀而已。
6.1 创建可执行的JAR包
当一个应用程序开发成功后,大致有三种发布方式:
1.使用平台相关的编译器将整个应用编译成平台相关的可执行性文件。这种方式常常需要第三方编译器支持,而且编译生成的可执行性文件丧失了跨平台特性,甚至可能有一定的性能下降。
2.为整个应用编辑一个批处理文件。使用如下命令:
java package.MainClass
当客户点击上面的批处理文件时候,系统执行批处理文件的java命令,从而允许程序的主类。
3.将一个应用程序制作成可执行的JAR包,通过JAR包来发布应用程序。
创建可执行JAR包的关键在于:让javaw命令知道JAR包中哪个类是主类,javaw命令可以通过运行该主类来运行程序,这就需要借助于清单文件,需要在清单文件中增加如下一行:
Main-Class:test.Test
也就是test包下的Test类作为主类。这样javaw就知道从JAR包中的test.Test开始运行。
在清单文件中增加这一行的方法如下:
1.创建一个文本文件,里面包含如下内容
Main-Class:<空格>test.Test<回车>
注意:上面的属性文件要求很严格,冒号前面是key(即Main-Class),冒号后面的空格的后面是value,也就是test.Test。文件格式要求如下:
1.每行只能写一个key-value对,key-value必须顶格写。
2.key-value之间的冒号后紧跟一个空格
3.文件开头没有空行
4.文件必须以一行空行结束,也就是末尾以回车结束。
上面的文件可以保存在任意位置,以任意文件名存放。
使用如下命令进行添加:
1 jar cvfm test.jar a.txt test
运行上面的命令后,在当前路径下生产一个test.jar文件,查看文件可以看到Main-Class为:test.Test。表名该JAR包的主类
6.2 运行JAR包有如下方式
1.使用java命令,使用java运行时的语法是:java -jar test.jar
2.使用javaw命令,使用javaw运行的语法是:javaw test.jar。