第5章面向对象高级特性本章学习目的与要求 本章主要讲解Java语言中一些高级特性。通过本章的学习,掌握一些用来修饰类成员的修饰符,抽象方法与抽象类的关系;掌握如何声明和实现一个或多个接口;认识内部类,掌握如何定义内部类,如何访问内部类;掌握自动装箱与拆箱以及枚举、注解和Lambda表达式等。 本章主要内容 本章主要介绍以下内容: 静态变量、静态方法。 最终类、常量。 抽象方法和抽象类。 接口。 内部类。 自动装箱与拆箱。 枚举。 注解。 Lambda表达式。 5.1静态变量、方法和初始化块 类定义的内容主要包括成员变量、成员方法、构造方法和初始化块等,在定义方法和变量时,可以在之前用关键字static(静态)修饰,表明它们是属于类的,称为静态方法(类方法)或静态变量(类变量)。 5.1.1静态变量 用static修饰的成员变量即为静态变量(类变量),若无static修饰的成员变量则是实例变量。静态变量或类变量是一种全局变量,它属于某个类,不属于某个对象实例,是在各对象实例之间共享。如果想访问静态变量可以直接通过类名来访问,可以不通过实例化访问它们。实例变量必须通过对象实例来访问它们。 【例51】静态变量与实例变量的访问。class Example05_1 { int a = 1; static int b = 2; } class Example05_1Demo { public static void main(String args\[\]) { System.out.println("b=" + Example05_1.b); Example05_1.b = 2; Example05_1 o1 = new Example05_1(); o1.a = 10; System.out.println("o1.a="+ o1.a); System.out.println("b="+ o1.b); Example05_1 o2=new Example05_1(); System.out.println("o2.a=" + o2.a); System.out.println("b="+ o2.b); } } 在类Example05_1中定义了两个成员变量a、b,其中变量b由static修饰,所以变量b为静态变量,如果想访问静态变量b,通过类名来访问Example05_1.b即可。如果想访问实例变量a,就要通过对象实例来访问o1.a或o2.a。静态变量b是在各对象实例间共享,而实例变量a是每个对象实例都独有的。 程序运行结果: b=2 o1.a=10 b=4 o2.a=1 b=4 Java程序设计与项目案例教程第5章Java面向对象高级特性5.1.2静态方法 同样,用static修饰的成员方法即为静态方法(类方法),调用静态方法可以通过类名来调用,即不用实例化即可调用它们。 【例52】静态方法的调用。class Example05_2{ public static int add(int x,int y){ return x+y; } } class Example05_2Test{ public void method(){ int a=1; int b=2; int c= Example05_2.add(a,b); } } add(int x,int y)方法是一个用static修饰的静态方法,所以可以通过类名来调用Example05_2.add(a,b)。 成员变量可以分为实例变量和静态变量两种;同样,方法也可以分为实例方法和静态方法两种。在方法使用变量时,需要注意以下规则: 实例变量或者类变量在定义初始化时,都不能超前引用。 实例方法既可以使用实例变量,又可以使用静态变量;而静态方法只能使用静态变量,不能直接使用实例变量。 【例53】以下实例使用了不正确的操作。class Example05_3{ String str="hello"; public static void main(String args\[\]){ System.out.println(str); } } 编译时错误信息如下: nonstatic variable str cannot be referenced from a static context "System.out.println(str);" 为什么不正确?因为静态方法不能直接使用实例变量。 解决的方法: 方法一: 将变量改成静态变量。例如:static String str="hello"; 方法二: 先创建一个类的实例,在用该实例调用实例变量。例如:System.out.println(new Example05_3().str); main()方法也是一个静态方法,为什么要把一个main()方法定义为一个静态方法?执行一个Java程序的时候,Java都是以类作为程序的组织单位,就是由类组成的,那么要执行的时候,并不知道这个main()方法要放到哪一个类当中,也不知道是否需要产生这个类的对象实例,为了解决程序的运行问题,就将main()方法定义为是静态方法,当加载所含main()方法的类时,main()方法就被加载了,不需要产生该类的对象;否则,如果main()方法被定义一个成员方法或非静态方法,那必须要产生一个类对象之后,才能调用main()方法,当然程序也就没有办法运行起来了。 5.1.3静态初始化块 类变量的初始化也可以通过静态初始化块来进行。静态初始化块是一个块语句,代码放置在一对大括号内,大括号前用关键字static修饰: static {…}一个类中可以定义一个或多个静态初始化块。静态初始化块会在加载类时调用而且只被调用一次。 【例54】静态初始化块。class Example05_4{ static int i = 5; static int j = 6; static int k; static void method() { System.out.println("k="+ k); } static{ if(i 5 >= j 4) k = 10; } public static void main(String args\[\]) { Example05_4.method(); } } 程序运行结果: k=10 method()方法是静态方法,所以最后一行代码Example05_4.method()通过类名就可以调用该方法,另外,类里定义的静态变量i、j、k和第8行的静态初始化块在类载入时就已经建立和执行初始化了。 5.2最终类、变量和方法 final(最终)也是一个重要的关键字,它可以用来修饰类、变量和方法。其中: 如果final在类之前,则表示该类不能被继承,即final类不能作为父类,任何final类中的方法自动成为final方法,一个类不能同时用abstract和final修饰。 如果final在方法之前,则防止该方法被覆盖,final方法不能被重写,因此,如果在子类中有一个同样签名的方法,那么将得到一个编译时错误。 如果final在变量之前,则定义一个常量,一个final变量的值不能被改变,并且必须在一定的时刻赋值。 【例55】用final修饰类,打印字符串“amethod”。final class Example05_5{ public void amethod(){ System.out.println("amethod"); } } class Fin { public static void main(String\[\] args){ Example05_5 b=new Example05_5(); b.amethod(); } } 程序运行结果: amethod 5.3抽象方法与抽象类 Java中可以定义一些不含方法体的方法,它的方法体的实现交给该类的子类根据自己的情况去实现,这样的方法就是abstract修饰符修饰的抽象方法。包含抽象方法的类就称为抽象类,也要用abstract修饰符修饰。 5.3.1抽象方法 用abstract来修饰一个方法时,该方法即为抽象方法。形式如下: abstract \[修饰符\] <返回类型>方法名称(\[参数表\]);抽象方法并不提供实现,即方法名称后面只有小括号而没有大括号的方法实现。包含抽象方法的类必须声明为抽象类,抽象超类的所有具体子类都必须为超类的抽象方法提供具体实现。 5.3.2抽象类 使用关键字abstract声明抽象类,形如: \[public\] abstract class 类名抽象类通常包含一个或多个抽象方法(静态方法不能为抽象方法)。 说明: 抽象类必须被继承,抽象方法必须被重写。 抽象类不能被直接实例化。因此,它一般作为其他类的超类,使用抽象超类来声明变量,用以保存抽象类所派生的任何具体类的对象的引用。程序通常使用这种变量来多态地操作子类对象。与final类正好相反。 抽象方法只须声明,无须实现。定义了抽象方法的类必须是用abstract修饰的抽象类。 如果声明如下一个类: class Shapes{ abstract double getArea(); void showArea(){ System.out.println("Area="+getArea()); } } 那么在编译的时候就会报错,因为在Shape类中定义了两个方法,其中getArea()方法是用abstract修饰的抽象方法,showArea()方法是成员方法,只要在类定义中出现至少一个抽象方法,那么该类也必须要定义成抽象类,所以要把Shape类也声明成抽象的,即第1行修改为abstract class Shapes{} 5.3.3扩展抽象类 抽象类不能被直接实例化,其目的是提供一个合适的超类,以派生其他类。 用于实例化对象的类称为具体类,这种类实现它们声明的所有方法。抽象超类是一般类,可以被看作是对其所有子类的共同行为的描述,并不创建出真实的对象。在创建对象之前,需要更为专业化的类,所以要得到抽象超类的任何一个具体类(非抽象子类),就必须提供该抽象类中的所有抽象方法的实现。例如: 【例56】扩展抽象类Example05_6使用如下。abstract class A { int x; abstract int m1(); abstract int m2(); } abstract class B extends A { int y; int m1() { return x+y; } } class C extends B { int z; int m2() { return x+y+z; } } 子类B和C会继承其父类中的所有方法(包括成员方法和抽象方法),那么这个子类只有覆盖实现其父类中的所有抽象(abstract)方法才能被定义成非抽象类(如子类C),否则也只能被定义成抽象类(如子类B)。 5.4接口 从本质上讲,接口(interface)是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义,而没有方法的实现。那么,为什么要使用接口呢?这是因为以下几个原因: 通过接口可以实现不相关类的相同行为,而无须考虑这些类之间的层次关系。 通过接口可以指明多个类需要实现的方法。 通过接口可以了解对象的交互界面,而无须了解对象所对应的类。 5.4.1接口的定义 在Java中,接口是由一些常量和抽象方法所组成的,接口中也只能包含这两种要素,一个接口的声明跟类的声明是一样的,只不过把关键字class换成了interface,接口定义的一般格式如下: \[public\] interface<接口名>\[extends<直接超接口名表>\]{ <类型><有名常量名>=<初始化表达式>;… <返回类型><方法名>(<行参表>);… }有public修饰的接口,能被任何包中接口或类访问;没有public修饰的接口,只能在所在包内被访问。 接口中的方法不提供具体的实现,其方法体用分号代替。其中,接口中的变量必须是用public static final修饰的,接口中的方法必须是用public static修饰的。如果不使用这些修饰符,Java编译器会自动加上。 【例57】接口定义示例。interface A{ void method1(int i); void method2(int j); } 在接口A中定义了两个方法,这两个方法虽然只有返回类型,但是系统会自动为这两个方法加上public static修饰符。 5.4.2接口的实现 有了接口之后,任何想要拥有接口所定义的类,就必须去实现这个接口。继承类是用extends关键字,而实现接口则使用implements关键字。在子类中可以使用接口中定义的常量,而且必须实现接口中定义的所有方法,否则子类就变为抽象类了。 下面是一个实现例57中接口A的实现类: class B implements A{ public void method1(int i){ …} public void method2(int j){ …} } 利用接口可以实现多重继承,即一个类可以实现多个接口,在implements子句中用逗号分隔。 【例58】一个类在继承同时实现多个接口。interface Sittable{ void sit(); } interface Lie{ void sleep(); } interface HealthCare{ void massage(); } class Chair implements Sittable{ public void sit(){}; } /interface Sofa extends Sittable,Lie//接口可以实现多重继承,用逗号相隔 { }/ class Sofa extends Chair implements Lie,HealthCare //一个类既可从父类中继承,同时又可实现多个接口 { public void sleep(){}; public void massage(){}; } 上面例58中定义了3个接口Sittable、Lie和HealthCare。定义了Chair类来实现Sittable接口;Sofa类继承自Chair接口,又实现了Lie和HealthCare接口。 接口最主要的功能是让不同实现的类拥有相同的访问方式,用户不需要知道具体是用什么方式实现的,只要知道这个接口提供了那些方法即可。 说明: 自JDK8以后,可以使用默认方法和静态方法这两个概念来扩展接口的声明。使用default关键字修饰默认方法,使用static关键字修饰静态方法,并提供默认的实现。接口的默认方法和静态方法的引入,其实可以认为是引入了C++中抽象类的理念,这样有利于代码的可复用性。 JDK8中能够在接口中定义带方法体的默认方法和静态方法,这样会导致一个问题: 当两个默认方法或者静态方法中包含一段相同的实现逻辑时,程序必然考虑将这段实现逻辑抽取成工具方法,而工具方法应该是隐藏的。所以,JDK9更新弥补了这一缺陷,增加了可带方法体的私有方法。 【例59】用默认方法和静态方法拓展接口的声明。package sample; interface DefaultImplementation { default void implementmethod() { System.out.println("default implementation"); } static void staticmethod() { System.out.println("static implemetation"); } class DefaultableImpl implements DefaultImplementation { } class OverridableImpl implements DefaultImplementation { public void implementmethod() { System.out.println("override default implementation"); } static void staticmethod(){ System.out.println("override static implemetation"); } } public static void main(String\[\] args) { DefaultableImpl dimpl = new DefaultableImpl(); OverridableImpl oimpl = new OverridableImpl(); dimpl.implementmethod(); oimpl.implementmethod(); oimpl.privatemethod(); } } 程序运行结果:default implementation override default implementation override static implemetation 5.5内部类 嵌套在另一个类中定义的类就是内部类(Inner Classes)。包含内部类的类称为外部类。 5.5.1认识内部类 内部类在Java最初的1.0版本中是不允许的,在后面的Java 1.1版本中才添加了内部类。内部类产生的原因主要是图形用户界面程序中的事件处理(将在后面详细讲解对于某些类型的事件内部类如何被用来简化代码)。它的存在的主要有两个目的: (1) 可以让程序设计中逻辑上相关的类结合在一起。有些类必须要伴随着另一个类的存在才有意义,如果两者分开,则可能在类的管理上比较麻烦,所以可以把这样的类写成一个Inner Class。 (2) Inner Class可以直接访问外部类的成员。因为Inner Class在外部类中,可以访问任何的成员,包括声明为private的成员。 下面的程序定义了一个内部类。创建内部类的基本语法如下。 【例510】内部类的声明。class Outer //这是一个外部类 { int outer_x = 100;//外部类成员 class Inner //这是一个内部类 { void display()//内部类成员 { System.out.println("display: outer_x = " + outer_x); } } } 在本程序中,类Inner嵌套在另一个Outer类中定义,类Inner就是内部类(Inner Classes)。包含内部类的Outer类就是外部类。 按照Inner Class声明的位置,可以把它分成两种: 一种是类成员式的,就像属性、方法一样,把一个类声明为另一个类的成员;另一种是区域式的,也就是把类声明在一个方法中。由这两种方式所派生出来的,按它的存在范围又分成4种级别,第一种是对象级别,第