当编写一个类时,经常会为该类定义一些方法,这些方法用以描述该类的行为方式,那么这些方法都有具体的方法体。但在某些状况下,某个父类并不须要实现,由于它只须要当作一个模板,而具体的实现,能够由它的子类来实现。好比说一个长方体的表面积和一个立方体的表面积计算方式是有区别的,长方体表面积须要有三个参数,而立方体须要一个参数。
抽象方法能够只有方法签名,而没有方法实现。java
抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里能够没有抽象方法。
抽象方法和抽象类的规则以下:
1.抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。连大括号都不能用
2.抽象类不能被实例化(实例化是为了调用属性和方法,抽象类自己没有方法实现的),没法使用new关键字来调用抽象类的构造器来初始化抽象类的实例。即便抽象类里不包含抽象方法,这个抽象类也不能建立实例。
3.抽象类能够包含属性、方法(普通方法和抽象方法均可以)、构造器、初始化块、内部类、枚举类六种成分。抽象类的构造器不能用于建立实例,主要用于被其子类调用
4.含义抽象方法的类(包括直接定义了一个抽象方法:继承了一个抽象父类,但没有彻底实现父类包含的抽象方法;以及实现了一个接口,但没有彻底实现接口包含的抽象方法三种状况)只能被定义成抽象类。
根据定义规则:普通类不能包含抽象方法,而抽象类不只能够包含普通方法,也能够包含抽象方法。程序员
abstract class 类名称{ 属性; 权限类型 返回值类型 方法名称(参数类别 参数列表){ } 权限类型 abstract 返回值类型 抽象方法名称(参数类型 参数列表) }
注意些抽象方法时候:只能是声明,后面连大括号{}都不能跟。大括号里面哪怕什么都没有,也表示是空语句,不是抽象方法。web
一个抽象类不能用final来声明,由于抽象类是须要继承来覆写抽象类的方法,可是final关键字意味着抽象类不能有子类,显然矛盾。数据库
抽象类的抽象方法也不能用private声明,由于抽象类的抽象方法须要子类覆写,使用private修饰的话,子类没法覆写抽象方法。编程
抽象方法和空方法体的方法不是同一个概念。例如public abstract void test();是一个抽象方法,它根本没有方法体,即方法定义后面没有一对花括号;但public void test(){}方法是一个普通方法,它已经定义了一个方法体,只是方法体为空,方法体上面也不作,这个方法不能用abstract来修饰。
abstract不能用于修饰属性,不能用于修饰局部变量,即没有抽象属性、抽象变量的说法,abstract也不能用于修饰构造器,抽象类里定义的构造器只能是普通构造器。
除此以外,当使用static来修饰一个方法时,表面这个方法属于当前类,即该方法能够经过类来调用,若是该方法被定义成抽象方法,则将致使经过该类来调用该方法时出现错误(调用了一个没有方法体的方法确定会引发错误),所以static和abstract不能同时修饰某个方法,即没有所谓的类抽象方法。
abstract关键字修饰的方法必须被其子类重写才有意义,不然这个方法将永远不会有方法体,所以abstract方法不能定义为private访问权限,即private和abstract不能同时使用。windows
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()); } }
从前面的实例能够看出,抽象类不能建立实例,它只能当成父类被继承。从语义角度来看,抽象类是从多个具体类抽象出来的父类,它具备更高层次的抽象。从多个具备相同特征的类中抽象出一个抽象类,以这个抽象类做为其子类的模板,从而避免了子类设计的随意性。
抽象类提醒的就是一种模板模式的设计,抽象类做为子类的模板,子类在抽象类的继承上进行扩展和改造。
若是编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给子类实现,这就是一种模板模式,模板模式也是最多见、最简单的设计模式之一。如前面的Shape、Circle和Triangle就是使用这种模式。设计模式
抽象类是从多个类抽象出来的模板,若是将这种抽象进行得更完全,则能够提炼出一种更加特殊的"抽象类"——接口(interface),接口里不能包含普通方法,接口里全部方法都是抽象方法。数组
常常据说接口,好比PCI接口、AGP接口,所以不少人认为接口等同于主机板上的插槽,这是一种错误的认识。当咱们说PCI接口时,指的是主机板上那条插槽遵照的了PCI规范,而具体的PCI插槽只是PCI接口的实例。
对于不一样型号的主机板而言,它们各自的PCI插槽都须要遵照一个规范,遵照这个规范就能够保证插入该插槽里的板卡能与主机板正常通讯。对于同一个型号的主机板而言,它们的PCI插槽都须要相同的数据交换方式、相同的实现细节,它们都是同一个类的不一样实例。
下图展现了三种层次的关系:接口层次、类层次和对象层次网络
从上图能够看出,同一个类的内部状态数据,各类方法的实现细节彻底相同,类是一种具体实现体。而接口定义了一种规范,接口定义某一批类所须要遵照的规范,接口不关心这些类的内部状态数据,也不须要关系这些类里方法的实现细节。它只规定这批类里必须提供某些方法,提供这些方法的类就可知足实际须要。
可见,接口是从多个类似类中抽象出来的规范,接口不提供任何实现。接口体现的是规范和实现分离的设计这些。闭包
让规范和实现分离是接口的好处,让软件系统的各组件之间面向接口耦合,是一种松耦合的设计。例如主机板上提供了PCI插槽,只要一块显卡遵照PCI接口规范,就能够插入PCI插槽内,与该主板正常通讯。至于这块显卡是哪一个厂家制造的,内部若是实现,主机板无需关心。
相似的,软件系统的各模块之间也应该采用面向接口的耦合,尽可能下降各模块之间的耦合,为系统提供更好的可扩展性和可维护性。
所以接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这就意味着接口里一般是定义一组共用方法。
和类定义不一样,定义接口再也不使用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; } }
接口的继承和类继承不一样,接口彻底支持多继承,即一个接口能够有多个直接父接口。和类继承类似,子接口扩展某个父接口,将得到父接口的全部抽象方法、常量属性、内部类和枚举类定义。
一个接口继承多个父接口时,多个父接口排在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); } }
接口不能用于建立实例,但接口能够用于声明引用类型的变量。当使用接口来声明引用类型的变量时,这个引用类型的变量必须引用到其实现类的对象。除此以外,接口主要用途是被实现类实现。
一个类能够实现一个或多个接口,继承使用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或其子类的实例。
接口和抽象都很像,它们都具备以下特征:
1. 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其余类实现和继承。
2. 接口和抽象类能够包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
但接口和抽象类之间的差异很大,这种差异主要体如今两者设计目的上,下面具体分析两者的差异。
接口做为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者能够调用哪些服务,当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通讯标准。
但接口和抽象类之间的差异很是大,这种差异主要体如今两者的设计目的上,下面具体分析二者的差异。
接口做为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者能够调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通讯标准。
从某种程度上来看,接口相似于整个系统的总纲,制定了系统各模块应该遵循的标准,所以一个系统中的接口不该该常常改变。一旦接口被改变,对整个系统甚至其余系统的影响将是辐射式的,致使系统中大部分类都须要改写。
抽象类则不同,抽象类做为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象父类做为多个子类的抽象父类,能够被当成系统实现过程当中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有跟进一步的完善,这种完善可能有几种不一样方式。
除此以外,接口和抽象类在用法上也存在以下区别。
1. 接口里只能包含抽象方法,不包含已经提供实现的方法;抽象类则彻底能够包含普通方法。
2. 接口里不能定义静态方法;抽象类里能够定义静态方法。
3. 接口里只能定义静态常量属性,不能定义普通属性;抽象类里则既能够定义普通属性,也能够定义静态常量属性。
4. 接口不包含构造器;抽象类里能够包含构造器,抽象类里的构造器并非用于建立对象,而让其子类调用这些构造器来完成属于抽象类的初始化操做。
5. 接口里不能包含初始化块,但抽象类则彻底能够包含初始化块。
6. 一个类最多只能有一个直接父类,包括抽象类;但一个类能够直接实现多个接口,经过实现多个接口能够弥补Java单继承的不足。
接口体现的是一种规范和设计分离的设计哲学,充分利用接口能够极好地下降程序各模块之间的耦合,从而提升系统的可扩展性和可维护性。
基于这种原则,不少软件架构设计理论都倡导面向接口编程,而不是面向实现类编程,但愿经过面向接口编程来下降程序的耦合。下面将介绍两种经常使用场景来示范面向接口编程的优点。
有这样的场景,假设程序中有个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程序中修改制造对象。
有这样的场景:某个方法须要完成某个行为,但这个行为的具体实现没法肯定,必须等到执行该方法时才能够肯定。具体一点:假设有个方法须要遍历某个数组的数组元素,但没法肯定在遍历数组时候还须要作什么操做,须要在调用该方法时指定具体的处理行为。这个看起来很奇怪,难道调用函数时候,可以把行为做为一个参数?在某些编程语言,如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()); } }
大部分时候,咱们把类定义成一个独立的程序单元。在某些状况下,咱们把一个类放在另外一个类的内部定义,这个定义在其余类内部的类被称为内部类(也被称为嵌套类),包含内部类的类也被称为外部类(有的地方也叫宿主类)。内部类有以下做用。
1.内部类提供了更好的封装,能够把内部类隐藏在外部类以内,不容许同一个包中的其余类访问该类。假设须要建立一个Cow类,Cow类须要组合一个CowLeg属性,CowLeg类只有在Cow类里才有效,离开Cow类以后没有任何意义。这种状况下就能够把CowLeg类定义成Cow的内部类,不容许其余类访问CowLeg
2.内部类成员能够直接访问外部类的私有数据,由于内部类被当成其外部类成员,同一个类的成员之间能够互相访问,但外部类不能访问内部类的实现细节,例如内部类的属性。
3.匿名内部类适合用于建立那些仅须要一次使用的类。对于前面介绍的命令模式,当须要传入一个Command对象时,从新专门定义PrintCommand和AddCommand两个实现类可能没有太大的意义,由于这两个实现类可能仅须要使用一次。这种状况下,匿名内部类使用更方便。
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 }
非静态内部类对象必须寄存在外部类对象里,而外部类对象则没必要必定有非静态内部类对象寄存其中。简单的说,若是存在一个非静态内部类对象,则必定存在一个被它寄存的外部类对象。但外部类对象存在时,外部类对象里不必定寄存了非静态内部类对象。所以外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在。而非静态内部类对象访问外部类成员时,外部类对象必定是存在的。
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量,建立实例等。总之,不能在外部类的静态成员中直接使用非静态内部类。
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 }
若是使用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访问控制权限。
接口里也能够定义内部接口,但这种作法用处不大,接口里的内部接口是接口的成员,接口的做用是定义一个公共规范,若是定义成一个内部接口,那么意义不大。
定义类的主要做用就是定义变量、建立实例和做为父类被继承。定义内部类的主要做用也如此。但使用内部类定义变量和建立实例则与外部类存在一些小小的差别。下面分三种状况讨论内部类的用法。
在前面程序中能够看出,在外部类的内部使用类时,与日常使用普通类没有太大区别。同样能够直接经过内部类类名来定义变量,经过new 调用内部类构造器来建立实例。
惟一存在的一个区别是:不要在外部类的静态成员(静态方法和静态初始化块)中使用非静态内部类,由于静态成员不能访问非静态成员。
在外部类内部定义内部类的子类与日常定义子类也没有太大的区别。
若是但愿在外部类之外的地方访问内部类(包括静态和非静态两种),则内部类不能使用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的构造器。
非静态内部类的子类不必定是内部类,也能够是顶层类。但非静态内部类的子类实例同样须要保留一个引用,该引用就是指向子类的父类所在的外部类的对象,也就是说,若是有一个内部类的子类对象存在,必定存在与之对应外部类的对象。
静态内部类是外部类相关的,所以建立内部类对象时无需建立外部类的对象。
语法是:
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{}
若是把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。所以,局部内部类不能在外部类之外的地方使用,那么局部内部类也无需使用访问控制符和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文件名增长一个数字用于区分。
匿名内部类适合一次性建立使用的类,例如命令模式的对象,建立匿名内部类时会当即建立该类的一个实例,这个类的定义当即消失,匿名内部类不能重复使用。
语法格式:
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();是不对的,由于接口没有构造器,没法进行实例化
闭包就是一种内部类,用内部类来实现外部接口,而且能够直接调用外部类的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 }
在某些状况下,一个类的对象是有限并且固定的,例如季节类,它只有四个对象。这种实例有限并且固定的类,在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 面朝大海,春暖花开~
枚举类也是一种类,只是它是一种比较特殊的类,所以它同样可使用属性和方法。
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属性,而应该经过方法来控制访问。
枚举类能够实现一个或多个接口。与普通类实现一个或多个接口彻底同样,枚举类实现一个或多个接口,也须要实现该接口所包含的方法。
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类的实例。
枚举类的枚举值就是实例值
假设有一个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方法,该方法可能使该对象从新复活(让一个引用该变量从新引用该对象),从而致使垃圾回收机制取消回收。
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,能够把它所处的状态分红以下三种:
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处于激活状态。
一个对象能够被一个方法局部变量所引用,也能够被其余类的类属性引用,或被其余对象的实例属性引用。当被类属性引用时,只有类被销毁,对象才会进入去活状态,当被其余对象的实例属性引用时,只有该对象被销毁,该对象才会进入去活状态。
程序没法精确控制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 }
在垃圾回收机制回收某个对象所占用的内存以前,一般要求程序调用适当的方法来清理资源,在没有明确指定资源清理的状况下,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方法。
对大部分对象而言,程序里会有一个引用变量引用该对象,这种引用方式是最多见的引用方式。除此以外,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 }
为了用 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包彻底同样,至少改变了文件后缀而已。
当一个应用程序开发成功后,大体有三种发布方式:
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包的主类
1.使用java命令,使用java运行时的语法是:java -jar test.jar2.使用javaw命令,使用javaw运行的语法是:javaw test.jar。