第5章 面向对象(下) 本章学习目标 理解继承的概念。 掌握final关键字的使用。 熟练掌握抽象类和接口的使用。 理解多态的概念。 掌握JDK 8.0中Lambda表达式的使用。 通过第4章的学习,相信读者对Java语言面向对象的基本知识已经有了初步了解,本章将介绍Java面向对象的另外两大特征: 继承和多态。此外,本章还将介绍final关键字、抽象类和接口、包、访问控制。 5.1类 的 继 承 继承是面向对象的另一大特征,用于描述类的所属关系,多个类通过继承形成一个关系体系。继承是在原有类的基础上扩展新的功能,实现了代码的复用。 5.1.1继承的概念 现实生活中,继承是指下一代人继承上一代人遗留的财产,即实现财产重用。在面向对象程序设计中,继承实现代码重用,即在已有类的基础上定义新的类,新的类能继承已有类的属性与行为,并扩展新的功能,而不需要把已有类的内容再写一遍。已有的类被称为父类或基类,新的类被称为子类或派生类。例如,交通工具与公交车就属于继承关系,公交车拥有交通工具的一切特性,但同时又拥有自己独有的特性。在Java中,子类继承父类的语法格式如下。 class 子类名 extends 父类名 { 属性和方法 } Java使用extends关键字指明两个类之间的继承关系。子类继承了父类中的属性和方法,也可以添加新的属性和方法,如例51所示。 例51TestExtends.java 1// 定义父类 2class Parent { 3String name;// 名称 4double property; // 财产 5public void say() { 6System.out.println(name+"的财产:"+ property); 7} 8} 9// 定义子类继承自父类 10class Child extends Parent { 11int age; 12public void sayAge() { 13System.out.println(name+"的年龄:"+ age); 14} 15} 16public class TestExtends { 17public static void main(String[] args) { 18// 创建Child对象 19Child c = new Child(); 20// Child对象本身没有name成员变量 21// 因为Child的父类有name成员变量,所以Child继承了父类的成员变量和方法 22c.name = "小明"; 23c.property = 100; 24c.age = 20; 25c.say (); 26c.sayAge(); 27} 28} 第 5 章 面向对象(下) Java语言程序设计(第2版) 程序的运行结果如图5.1所示。 图5.1例51运行结果 在例51中,Child类通过extends关键字继承了Parent类,Child类便是Parent的子类。从程序运行结果可发现,Child类虽然没有定义name、property成员变量和say()成员方法,但却能访问这些成员,说明子类可以继承父类所有的成员。另外,在Child类里还定义了一个sayAge()方法,说明子类可以扩展父类功能。 Java语言只支持单继承,不允许多重继承,即一个子类只能继承一个父类,否则会引起编译错误,具体示例如下。 class A {} class B {} class C extends A, B {} Java语言虽然不支持多重继承,但它支持多层继承,即一个类的父类可以继承另外的父类。因此,Java类可以有无限多个间接父类,具体示例如下。 class A {} class B extends A {} class C extends B {} 5.1.2重写父类方法 在继承关系中,子类从父类中继承了可访问的方法,但有时从父类继承下来的方法不能完全满足子类需要,例如例51中,如果要求父类与子类中的say()方法输出不同内容,这时就需要在子类的方法里修改父类的方法,即子类重新定义从父类中继承的成员方法,这个过程称为方法重写或覆盖。在进行方法重写时必须考虑权限,即被子类重写的方法不能拥有比父类方法更加严格的访问权限,如例52所示。 例52TestOverride.java 1// 定义父类 2class Parent { 3protected void say() { 4System.out.println("父辈"); 5} 6} 7// 定义子类继承自父类 8class Child extends Parent { 9public void say() { 10System.out.println("子女"); 11} 12} 13public class TestOverride { 14public static void main(String[] args) { 15// 创建Child对象 16Child c = new Child(); 17c.say(); 18} 19} 程序的运行结果如图5.2所示。 图5.2例52运行结果 在例52中,Child类继承了Parent类的say()方法,但在子类Child中重新定义的say()方法对父类的say()进行了重写。从程序运行结果可发现,在调用Child类对象的say()方法时,只会调用子类重写的方法,并不会调用父类的say()方法。 另外,需要注意方法重载与方法重写的区别。 (1) 方法重载是在同一个类中,方法重写是在子类与父类中。 (2) 方法重载要求: 方法名相同,参数个数或参数类型不同。 (3) 方法重写要求: 子类与父类的方法名、返回值类型和参数列表相同。 5.1.3super关键字 当子类重写父类方法后,子类对象将无法访问父类被重写的方法。如果在子类中需要访问父类的被重写方法,可以通过super关键字来实现,其语法格式如下。 super.成员变量 super.成员方法([实参列表]) 接下来演示super关键字的作用,如例53所示。 例53TestSuper.java 1// 定义父类 2class Parent { 3String name="Parent";// 名称 4public void say() { 5System.out.println("父辈"); 6} 7} 8// 定义子类继承自父类 9class Child extends Parent { 10public void say() { 11String name=super.name; // 访问父类成员变量 12super.say(); // 访问父类成员方法 13System.out.println("姓名:" + name); 14} 15} 16public class TestSuper{ 17public static void main(String[] args) { 18// 创建Child对象 19Child c = new Child(); 20c.say(); 21} 22} 程序的运行结果如图5.3所示。 图5.3例53运行结果 在例53中,Child类继承自Parent类,并重写了say()方法。在子类Child的say()方法中,使用super.name调用了父类的成员变量,使用super.say()调用了父类被重写的成员方法。从程序运行结果可发现,通过super关键字可以在子类中访问被隐藏的父类成员。 在继承中,实例化子类对象时,首先会调用父类的构造方法,再调用子类的构造方法,这与实际生活中先有父母再有孩子类似。子类继承父类时,并没有继承父类的构造方法,但子类构造方法可以调用父类的构造方法。在一个构造方法中调用另一个重载的构造方法时应使用this关键字,在子类构造方法中调用父类的构造方法时应使用super关键字,其语法格式如下。 super([参数列表]) 接下来演示super调用父类的构造方法,如例54所示。 例54TestSuperRefConstructor.java 1// 定义父类 2class Parent { 3String name;// 名称 4public Parent(String name) { 5this.name = name; 6} 7public void say() { 8System.out.println("父辈"); 9} 10} 11// 定义子类继承自父类 12class Child extends Parent { 13public Child() { 14super("Parent"); 15} 16public void say() { 17System.out.println("姓名:" + name); 18} 19} 20public class TestSuperRefConstructor { 21public static void main(String[] args) { 22// 创建Child对象 23Child c = new Child(); 24c.say(); 25} 26} 程序的运行结果如图5.4所示。 图5.4例54运行结果 从程序运行结果可发现,实例化Child类对象时调用了父类的有参构造方法。super关键字调用构造方法和this关键字调用构造方法类似,该语句必须位于子类构造方法的第一行,否则会编译出错。 另外,子类中如果没有显式地调用父类的构造方法,那么将自动调用父类中不带参数的构造方法,如例55所示。 例55TestAutoCallConstructor.java 1// 定义父类 2class Parent { 3String name; 4public Parent(String name) { 5this.name = name; 6System.out.println("Parent(String name)"); 7} 8} 9// 定义子类继承自父类 10class Child extends Parent { 11public Child() { 12System.out.println("Child()"); 13} 14public void say() { 15System.out.println("姓名:" + name); 16} 17} 18public class TestAutoCallConstructor { 19public static void main(String[] args) { 20// 创建Child对象 21Child c = new Child(); 22} 23} 程序的运行结果如图5.5所示。 图5.5例55运行结果 在图5.5中,程序编译结果报错,原因是在Child类的构造方法中没有显式地调用父类构造方法,便会默认调用父类无参构造方法,而父类Parent中显式定义了有参构造方法,此时编译器将不再自动生成默认构造方法。因此,程序找不到无参构造方法而报错。 为了解决上述程序的编译错误,可以在子类中显式地调用父类中定义的构造方法,也可以在父类中显式定义无参构造方法,如例56所示。 例56TestConstructorOrder.java 1// 定义父类 2class Parent { 3String name; 4// 定义无参构造方法 5public Parent() { 6System.out.println("Parent()"); 7} 8public Parent(String name) { 9this.name = name; 10System.out.println("Parent(String name)"); 11} 12} 13// 定义子类继承自父类 14class Child extends Parent { 15public Child() { 16System.out.println("Child()"); 17} 18public void say() { 19System.out.println("姓名:" + name); 20} 21} 22public class TestConstructorOrder { 23public static void main(String[] args) { 24// 创建Child对象 25Child c = new Child(); 26} 27} 程序的运行结果如图5.6所示。 图5.6例56运行结果 在图5.6中,从程序运行结果可发现,子类在实例化时默认调用父类的无参构造方法,并且父类的构造方法在子类构造方法之前执行。 5.2final关键字 在Java中,为了考虑安全因素,要求某些类不允许被继承或不允许被子类修改,这时可以用final关键字修饰。它可用于修饰类、方法和变量,其具体特点如下。 (1) final修饰的类不能被继承。 (2) final修饰的方法不能被子类重写。 (3) final修饰的变量是常量,初始化后不能再修改。 5.2.1final关键字修饰类 使用final关键字修饰的类称为最终类,表示不能再被其他的类继承,如Java中的String类。接下来演示final修饰类,如例57所示。 例57TestFinalClass.java 1// 使用final关键字修饰类 2final class Parent { 3} 4// 继承final类 5class Child extends Parent { 6} 7public class TestFinalClass { 8public static void main(String[] args) { 9// 创建Child对象 10Child c = new Child(); 11} 12} 程序的运行结果如图5.7所示。 图5.7例57运行结果 在例57中,使用final关键字修饰了Parent类。因此,Child类继承Parent类时,程序编译结果报错并提示“无法从最终Parent类进行继承”。由此可见,被final修饰的类为最终类,不能再被继承。 5.2.2final关键字修饰方法 使用final关键字修饰的方法,称为最终方法,表示子类不能重写此方法,接下来演示final修饰方法,如例58所示。 例58TestFinalFunction.java 1class Parent { 2// final关键字修饰方法 3public final void say() { 4System.out.println("final修饰say()方法"); 5} 6} 7class Child extends Parent { 8// 重写父类方法 9public void say() { 10System.out.println("重写父类say()方法"); 11} 12} 13public class TestFinalFunction { 14public static void main(String[] args) { 15// 创建Child对象 16Child c = new Child(); 17c.say(); 18} 19} 程序的运行结果如图5.8所示。 图5.8例58运行结果 在例58中,Parent类中使用final关键字修饰了成员方法say(),Child类继承Parent类并重写了say()方法。程序编译结果报错并提示被覆盖的方法为final。由此可见,被final修饰的成员方法为最终方法,不能再被子类重写。 5.2.3final关键字修饰变量 使用final关键字修饰的变量,称为常量,只能被赋值一次。如果再次对该变量进行赋值,则程序在编译时会报错,如例59所示。 例59TestFinalLocalVar.java 1package test; 2public class TestFinalLocalVar { 3public static void main(String[] args) { 4final double PI = 3.14;// 定义并初始化 5PI = 3.141592653; // 重新赋值 6} 7} 程序的运行结果如图5.9所示。 图5.9例59运行结果 在例59中,使用final修饰变量PI,再次对其进行赋值,程序编译结果报错并提示“无法为最终变量PI分配值”。由此可见,final修饰的变量为常量,只能初始化一次,初始化后不能再修改。 在例59中,使用final修饰的是局部变量,接下来使用final修饰成员变量,如例510所示。 例510TestFinalMemberVar.java 1class Parent { 2// 使用final修饰成员变量 3final double PI; 4public void say() { 5System.out.println(this.PI); 6} 7} 8class Child extends Parent { 9} 10public class TestFinalMemberVar { 11public static void main(String[] args) { 12// 创建Child对象 13Child c = new Child(); 14c.say(); 15} 16} 程序的运行结果如图5.10所示。 图5.10例510运行结果 在例510中,Parent类中使用final修饰了成员变量PI,程序编译结果报错并提示可能尚未初始化变量PI。由此可见,Java虚拟机不会为final修饰的变量默认初始化。因此,使用final修饰成员变量时,需要在声明时立即初始化,或者在构造方法中进行初始化。下面使用构造方法初始化final修饰的成员变量,在Parent类中添加代码,具体如下。 public Parent() {// 构造方法中初始化final成员变量 PI = 3.14; } 程序的运行结果如图5.11所示。 图5.11例510修改后运行结果 此外,final关键字还可以修饰引用变量,表示该变量只能始终引用一个对象,但可以改变对象的内容,有兴趣的读者可以动手验证一下。 5.3抽象类和接口 5.3.1抽象类 Java中可以定义不含方法体的方法,方法的方法体由该类的子类根据实际需求去实现,这样的方法称为抽象方法,包含抽象方法的类必须是抽象类。 Java中提供了abstract关键字,表示抽象的意思,用abstract修饰的方法,称为抽象方法,是一个不完整的方法,只有方法的声明,没有方法体。用abstract修饰的类,称为抽象类,抽象类可以不包含任何抽象方法,具体示例如下。 // 用abstract修饰抽象类 abstract class Parent { // abstract修饰抽象方法,只有声明,没有实现 public abstract void say(); } 使用抽象类时需要注意,抽象类不能被实例化,即不能用new关键字创建对象,因为抽象类中可包含抽象方法,抽象方法只有声明没有方法体,不能被调用。因此,必须通过子类继承抽象类去实现抽象方法,如例511所示。 例511TestAbstractClass.java 1// 用abstract修饰抽象类 2abstract class Parent { 3// abstract修饰抽象方法,只有声明,没有实现 4public abstract void say(); 5} 6// 继承抽象类 7class Child extends Parent { 8// 实现抽象方法 9public void say() { 10System.out.println("Child"); 11} 12} 13public class TestAbstractClass { 14public static void main(String[] args) { 15Child c = new Child(); 16c.say(); 17} 18} 程序的运行结果如图5.12所示。 图5.12例511运行结果 在例511中,子类定义时实现了抽象方法,因此在主方法实例化子类对象后,子类对象可以调用子类中实现的抽象方法。 需要注意的是,具体子类必须实现抽象父类中的所有抽象方法,否则子类必须要声明为抽象类,如例512所示。 例512TestAbstractFun.java 1// 用abstract修饰抽象类 2abstract class Parent { 3// abstract修饰抽象方法,只有声明,没有实现 4public abstract void say(); 5public abstract void work(); 6} 7// 继承抽象 8class Child extends Parent { 9// 实现抽象方法 10public void say() { 11System.out.println("Child"); 12} 13} 14public class TestAbstractFun { 15public static void main(String[] args) { 16Child c = new Child(); 17c.say(); 18} 19} 程序的运行结果如图5.13所示。 图5.13例512运行结果 在例512中,子类Child中只实现了抽象类Parent的say()抽象方法,而未实现work()抽象方法。程序编译结果报错并提示“Child不是抽象的,并且未覆盖Parent中的抽象方法work()”。错误的原因在于: 子类继承了抽象类的work()抽象方法,该方法没有方法体,不能被实例化。因此,子类必须实现抽象类的全部抽象方法,否则子类必须声明为抽象类。 另外,抽象方法不能用static来修饰,因为static修饰的方法可以通过类名调用,调用时将调用一个没有方法体的方法,肯定会出错; 抽象方法也不能用final关键字修饰,因为被final关键字修饰的方法不能被重写,而抽象方法的实现需要在子类中实现; 抽象方法也不能用private关键字修饰,因为子类不能访问带private关键字的抽象方法。 抽象类中可以定义构造方法,因为抽象类仍然使用的是类继承关系,而且抽象类中也可以定义成员变量。因此,子类在实例化时必须先对抽象类进行实例化,如例513所示。 例513TestAbstractConstructor.java 1// 用abstract修饰抽象类 2abstract class Parent { 3// abstract修饰抽象方法,只有声明,没有实现 4private String name; 5public Parent() { 6System.out.println("抽象类无参构造方法"); 7} 8public Parent(String name) { 9this.name = name; 10System.out.println("抽象类有参构造方法"); 11} 12} 13// 继承抽象方法 14class Child extends Parent { 15public Child() { 16System.out.println("子类无参构造函数"); 17} 18public Child(String name) { 19super(name);// 显示调用抽象类有参构造函数 20System.out.println("子类有参构造函数"); 21} 22} 23public class TestAbstractConstructor { 24public static void main(String[] args) { 25new Child(); 26new Child("张三"); 27} 28} 程序的运行结果如图5.14所示。 图5.14例513运行结果 在例513中,抽象类Parent中定义了无参和有参两个构造方法,从运行结果可以看出,在子类对象实例化时会默认调用抽象父类中的无参构造方法,也能直接通过super关键字调用抽象父类中指定参数的构造方法。 5.3.2接口 接口是全局常量和公共抽象方法的集合,可被看作一种特殊的类,也属于引用类型。每个接口都被编译成独立的字节码文件。Java提供interface关键字,用于声明接口,其语法格式如下。 interface 接口名{ 全局常量声明 抽象方法声明 } 接下来演示interface关键字的作用,具体示例如下。 // 用interface声明接口 interface Parent { String name;// 等价于public static final String name; void say();// 等价于public abstract void say(); } 接口中定义的变量和方法都包含默认的修饰符,其中定义的变量默认声明为“public static final”,即全局常量。另外,定义的方法默认声明为“public abstract”,即抽象方法。 5.3.3接口的实现 与抽象类相似,接口中也包含抽象方法。因此,不能直接实例化接口,即不能使用new创建接口的实例。Java提供implements关键字,用于实现多个接口,具体示例如下。 class 类名 implements 接口列表{ 属性和方法 } 接下来演示接口的实现,如例514所示。 例514TestImplements.java 1// 用interface声明接口 2interface Person { 3void say(); 4} 5interface Parent { 6void work(); 7} 8// 用implements实现两个接口 9class Child implements Person, Parent { 10public void work() { 11System.out.println("学习"); 12} 13public void say() { 14System.out.println("Child"); 15} 16} 17public class TestImplements{ 18public static void main(String[] args) { 19Child c = new Child(); 20c.say(); 21c.work(); 22} 23} 程序的运行结果如图5.15所示。 图5.15例514运行结果 在例514中,使用interface定义了两个接口,并在声明Child类的同时使用implements实现了接口Person和Parent,接口名之间用逗号分隔,类中实现了接口中所有的抽象方法。从程序运行结果可发现,Child类实现了接口且可以被实例化。 5.3.4接口的继承 在Java中使用extends关键字来实现接口的继承,它与类的继承类似,当一个接口继承父接口时,该接口会获得父接口中定义的所有抽象方法和常量,但又与类的继承不同,接口支持多重继承,即一个接口可以继承多个父接口。其语法格式如下。 interface 接口名 extends 接口列表 { 全局常量声明 抽象方法声明 } 接下来演示接口之间的继承关系,如例515所示。 例515TestInterfaceExtend.java 1// 用interface声明接口 2interface Person { 3void say(); 4} 5// 用extends继承接口 6interface Parent extends Person { 7void work(); 8} 9// 用implements实现两个接口 10class Child implements Parent { 11public void work() { 12System.out.println("学习"); 13} 14public void say() { 15System.out.println("Child"); 16} 17} 18public class TestInterfaceExtend { 19public static void main(String[] args) { 20Child c = new Child(); 21c.say(); 22c.work(); 23} 24} 程序的运行结果如图5.16所示。 图5.16例515运行结果 在例515中定义了两个接口,其中Parent接口继承了Person接口,当Child类实现Parent接口时,需要实现父类接口中所有的方法。需要特别指出的是,任何实现继承接口的类,必须实现该接口继承的其他接口,除非类被声明为abstract。 5.3.5抽象类和接口的关系 抽象类与接口是Java语言中对于抽象类定义进行支持的两种机制,两者非常相似,初学者经常混淆这两个概念,两者的相同点可以归纳为以下三点。 (1) 都包含抽象方法。 (2) 都不能被实例化。 (3) 都是引用类型。 表5.1列出了两者之间的区别。 表5.1接口与抽象类 区别点接口抽象类 含义接口通常用于描述一个类的外围能力,而不是核心特征。类与接口之间是able或者can do的关系抽象类定义了它的后代的核心特征。派生类与抽象类之间是isa的关系 方法接口只提供方法声明抽象类可以提供完整方法、默认构造方法以及用于覆盖的方法声明 变量只包含public static final常量,常量必须在声明时初始化可以包含实例变量和静态变量 多重继承一个类可以继承多个接口一个类只能继承一个抽象类 实现类类可以实现多个接口类只从抽象类派生,必须重写 适用性所有的实现只是共享方法签名所有实现大同小异,并且共享状态和行为 简洁性接口中的常量都被默认为public static final,可以省略。接口中的方法被默认为public abstract可以在抽象类中放置共享代码。必须用abstract显式声明方法为抽象方法 添加功能如果为接口添加一个新的方法,则必须查找所有实现该接口的类,并为它们逐一提供该方法的实现如果为抽象类提供一个方法,可以选择提供一个默认的实现,那么所有已存在的代码不需要修改就可以继续工作 总体来说,抽象类和接口都用于为对象定义共同的行为,两者在很大程度上是可以互相替换的,但由于抽象类只允许单继承,所以当两者都可以使用时,优先考虑接口,只有当需要定义子类的行为,并为子类提供共性功能时才考虑选用抽象类。 5.4多态 多态是面向对象的另一大特征,封装和继承是为实现多态做准备的。简单来说,多态是具有表现多种形态能力的特征,它可以提高程序的抽象程度和简洁性,最大程度降低了类和程序模块间的耦合性。 5.4.1多态的概念 多态是指同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在Java程序中,多态是指把类中具有相似功能的不同方法使用同一个方法名实现,从而可以使用相同的方式来调用这些具有不同功能的同名方法。接下来通过一个案例演示多态的实现,如例516所示。 例516TestPolymorphism.java 1// 定义Person类 2class Person { 3public void say() { 4System.out.println("Person"); 5} 6} 7// 定义Parent类继承Person类 8class Parent extends Person { 9public void say() { 10System.out.println("Parent"); 11} 12} 13// 定义Child类实现Parent类 14class Child extends Parent { 15public void say() { 16System.out.println("Child"); 17} 18} 19public class TestPolymorphism { 20public static void main(String[] args) { 21// 定义Person类型引用变量 22Person p = null; 23// 使用Person类型变量引用Parent对象 24p = new Parent(); 25p.say(); 26// 使用Person类型变量引用Child对象 27p = new Child(); 28p.say(); 29// 使用Parent类型变量引用Child对象 30Parent p2 = new Child(); 31p2.say(); 32} 33} 程序的运行结果如图5.17所示。 图5.17例516运行结果 在例516中,主方法中实现了父类类型变量引用不同的子类对象,其中第27行代码处,变量p引用的是Parent对象,因此,此处调用的是Parent类里的say()方法; 第28行代码处,变量p引用的是Child对象,因此,此处调用的是Child类里重写的say()方法。从程序运行结果可发现,虽然执行的都是“p.say(); ”语句,但变量引用的对象是不同的,执行的结果也不同,这就是前面所讲的多态。 Java中的引用变量有两种类型,即声明类型和实际类型。变量声明时被指定的类型,称为声明类型,而被变量引用的对象类型,称为实际类型。方法可以在沿着继承链的多个类中实现,当调用实例方法时,由Java虚拟机动态地决定所调用的方法,称为动态绑定。 动态绑定机制原理是: 当调用实例方法时,Java虚拟机从该变量的实际类型开始,沿着继承链向上查找该方法的实现,直到找到为止,并调用首次找到的实现,如例517所示。 例517TestDynamicBinding.java 1// 定义Person类 2class Person { 3public void say() { 4System.out.println("Person"); 5} 6} 7// 定义Parent类继承Person类 8class Parent extends Person { 9public void say() { 10System.out.println("Parent"); 11} 12} 13// 定义Child类继承Parent类 14class Child extends Parent{ 15} 16public class TestDynamicBinding { 17public static void main(String[] args) { 18// 使用Person类型变量引用Child对象 19Person p = new Child(); 20p.say(); 21} 22} 程序的运行结果如图5.18所示。 图5.18例517运行结果 在例517中,Person类和Parent类都实现了say()方法,Child是空类,三个类构成了继承链。主方法中调用say()方法时,Java虚拟机沿着继承链向上查找实现并运行。因此,运行结果打印了“Parent”。由此可见,Java虚拟机在运行时动态绑定方法的实现,是由变量的实际类型决定的。 5.4.2对象的类型转换 对象的类型转换是指可以将一个对象的类型转换成继承结构中的另一种类型。类型转换分为两种,具体如下。 (1) 向上转型,是从子类到父类的转换,也称隐式转换。 (2) 向下转型,是从父类到子类的转换,也称显式转换。 接下来演示对象的类型转换,如例518所示。 例518TestTypeCast.java 1// 定义Person类 2class Person { 3public void say() { 4System.out.println("Person"); 5} 6} 7// 定义Parent类继承Person类 8class Parent extends Person { 9public void say() { 10System.out.println("Parent"); 11} 12} 13// 定义Child类继承Parent类 14class Child extends Parent{ 15public void say() { 16System.out.println("Child"); 17} 18} 19public class TestTypeCast { 20public static void main(String[] args) { 21Person p = new Child();// 向上转型 22Parent o = (Parent) p;// 向下转型 23o.say(); 24} 25} 程序的运行结果如图5.19所示。 图5.19例518运行结果 在例518中,定义三个类构成继承链,创建Child对象并赋给Person的变量p,是隐式的转换,称为向上转型; 再将变量p赋给Parent的变量o,必须强制转换,称为向下转型。从程序运行结果可发现,对象向下转型后,调用方法还是由实际对象决定的。 需要特别注意的是,向下转型时,被转换变量的实际类型,必须是转换类或其子类,如例519所示。 例519TestInstanceof.java 1// 定义Person类 2class Person { 3public void say() { 4System.out.println("Person"); 5} 6} 7// 定义Parent类继承Person类 8class Parent extends Person { 9public void say() { 10System.out.println("Parent"); 11} 12} 13// 定义Child类继承Person类 14class Child extends Person { 15public void say() { 16System.out.println("Child"); 17} 18} 19public class TestInstanceof { 20public static void main(String[] args) { 21Person p = new Child();// 向上转型 22Parent o = (Parent) p;// 向下转型 23o.say(); 24} 25} 程序的运行结果如图5.20所示。 图5.20例519运行结果 在图5.20中,程序运行结果报错并提示“Child cannot be cast to Parent”。报错的原因在于: Person类型变量p先指向Child对象,再向下转型为Parent类型。在编译时,编译器检测的是变量的声明类型,则编译可以通过。但在运行时,转换的是变量的实际类型,而Child类型无法强制转换为Parent类型。 针对这种情况,Java提供了instanceof关键字,用于判断一个对象是否是一个类(或接口)的实例,表达式返回boolean值,其语法格式如下。 变量名instanceof类名 接下来对例519的主方法进行修改,具体代码如下。 public static void main(String[] args) { Person p = new Child();// 向上转型 if (p instanceof Parent) {// 判断对象是否是Parent类型 Parent o = (Parent) p; o.say(); } else if (p instanceof Child) {// 判断对象是否是Child类型 Child o = (Child) p; o.say(); } } 程序的运行结果如图5.21所示。 图5.21例519修改后运行结果 从程序运行结果可发现,instanceof能准确判断出对象是否是某个类的实例。 5.4.3Object类 Java中提供了一个Object类,是所有类的父类,如果一个类没有显式地指定继承类,则该类的父类默认为Object。例如,下面两个类的定义是一样的。 class ClassName {} class ClassName extends Object {} 在Object类中提供了很多方法,接下来分别对其中的方法进行解释,如表5.2所示。 本章暂时只对toString()和equals方法进行讲解,而hashCode()方法在Java集合中再详细讲解。 表5.2Object类的方法 方 法 声 明功 能 描 述 public String toString()返回描述该对象的字符串 public Boolean equals(Object o)比较两个对象是否相等 public int hashCode()返回对象的哈希值 1. toString()方法 调用一个对象的toString()方法会默认返回一个描述该对象的字符串,它由该对象所属类名、@和对象十六进制形式的内存地址组成,如例520所示。 例520TestToString.java 1class Person { 2private String name; 3private int age; 4public Person(String name, int age) { 5this.name = name; 6this.age = age; 7} 8} 9public class TestToString { 10public static void main(String[] args) { 11Person o = new Person("张三", 18); 12// 调用对象的toString方法 13System.out.println(o.toString()); 14// 直接打印对象 15System.out.println(o); 16} 17} 程序的运行结果如图5.22所示。 图5.22例520运行结果 在图5.22中,默认打印了对象信息,从程序运行结果可发现,直接打印对象和打印对象的toString()方法返回值相同,也就是说,对象输出一定会调用Object类的toString()方法。 通常,重写toString()方法返回对象具体的信息。修改例520中的Person类,添加重写toString()方法的代码,具体示例如下。 public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } 程序的运行结果如图5.23所示。 图5.23例520修改后运行结果 在图5.23中,从程序运行结果可发现,程序调用的是子类重写Object类的toString()方法,这是多态机制的体现。 2. equals()方法 equals()方法是用于测试两个对象是否相等,如例521所示。 例521TestEquals.java 1class Person { 2private String name; 3private int age; 4public Person(String name, int age) { 5this.name = name; 6this.age = age; 7} 8// 自定义比较方法,检查两个引用变量是否指向同一对象 9public boolean myEquals(Object o) { 10return (this == o); 11} 12} 13public class TestEquals { 14public static void main(String[] args) { 15Person o1 = new Person("张三", 18); 16Person o2 = new Person("张三", 18); 17// 调用对象的equals方法 18System.out.println(o1.equals(o2)); 19// 调用自定义myEquals方法 20System.out.println(o1.myEquals(o2)); 21} 22} 程序的运行结果如图5.24所示。 图5.24例521运行结果 在图5.24中,从程序运行结果可发现,equals()方法与直接使用==运算符检测两个对象结果相同。这是由于equals()方法的默认实现就是用==运算符检测两个引用变量是否指向同一个对象,即比较的是地址。 如果要检测两个不同对象的内容是否相同,就必须重写equals()方法。例如,String类中的equals()方法继承自Object类并重写,使之能够检验两个字符串的内容是否相等。修改例521中的Person类,添加重写equals()方法的代码,具体示例如下。 public boolean equals(Object o) { // 比较对象是否是自己 if (this == o) return true; // 判断是否为该类对象 if (!(o instanceof Person)) return false; Person p = (Person) o; // 逐个属性比较,看是否相等 if ( ( // 比较name属性 (null == name && null == p.name)// 同为null的情况 || (null != name && name.equals(p.name)) ) && age == p.age // 比较age属性 ) return true; return false; } 程序的运行结果如图5.25所示。 图5.25例521修改后运行结果 在例521中,在Person类中重写了equals()方法,首先比较两个对象的地址是否相等,如果相等,则是同一对象。因为equals()方法的形参是Object类型,所以可以接收任何对象。因此,必须要判断对象是否是Person的实例,如果是,则依次比较各个属性。 5.4.4设计模式——工厂设计模式 工厂模式(Factory)主要用来实例化有共同接口的类,它可以动态决定应该实例化哪一个类,不必事先知道每次要实例化哪一个类。工厂模式主要有三种形态: 简单工厂模式、工厂方法模式和抽象工厂模式。接下来分别对这三种形态进行讲解。 1. 简单工厂模式 简单工厂模式(Simple Factory Pattern)又称静态工厂方法,它的核心是类中包含一个静态方法,该方法用于根据参数来决定返回实现同一接口不同类的实例,如例522所示。 例522TestSimpleFactoryPattern.java 1// 定义产品接口 2interface Product { 3} 4// 定义安卓手机类 5class Android implements Product { 6public Android() { 7System.out.println("安卓手机被创建!"); 8} 9} 10// 定义苹果手机类 11class Iphone implements Product { 12public Iphone() { 13System.out.println("苹果手机被创建!"); 14} 15} 16// 定义工厂类 17class SimpleFactory { 18public static Product factory(String className) { 19if ("Android".equals(className)) { 20return new Android(); 21} else if ("Iphone".equals(className)) { 22return new Iphone(); 23} else { 24return null; 25} 26} 27} 28public class TestSimpleFactoryPattern { 29public static void main(String[] args) { 30// 根据不同的参数生成产品 31SimpleFactory.factory("Android"); 32SimpleFactory.factory("Iphone"); 33} 34} 程序的运行结果如图5.26所示。 图5.26例522运行结果 在例522中,定义SimpleFactory类就是简单工厂的核心,该类拥有必要的逻辑判断和创建对象的责任。由此可见,简单工厂就是将创建产品的操作集中在一个类中。 工厂类SimpleFactory有很多局限。首先,维护和新增产品时,都必须修改SimpleFactory源代码。其次,如果产品之间存在复杂的层次关系,则工厂类必须拥有复杂的逻辑判断。最后,整个系统都依赖SimpleFactory类,一旦SimpleFactory类出现问题,整个系统就将瘫痪不能运行。 2. 工厂方法模式 工厂方法模式(Factory Method Pattern)为工厂类定义了接口,用多态来削弱了工厂类的职责,如例523所示。 例523TestFactoryMethodPattern.java 1// 定义产品接口 2interface Product { 3} 4// 定义安卓手机类 5class Android implements Product { 6public Android() { 7System.out.println("安卓手机被创建!"); 8} 9} 10// 定义苹果手机类 11class Iphone implements Product { 12public Iphone() { 13System.out.println("苹果手机被创建!"); 14} 15} 16// 定义工厂接口 17interface Factory { 18public Product create(); 19} 20// 定义Android的工厂 21class AndroidFactory implements Factory { 22public Product create() { 23return new Android(); 24} 25} 26// 定义Iphone的工厂 27class IphoneFactory implements Factory { 28public Product create() { 29return new Iphone(); 30} 31} 32public class TestFactoryMethodPattern { 33public static void main(String[] args) { 34// 根据不同的子工厂创建产品 35Factory factory = null; 36factory = new AndroidFactory(); 37factory.create(); 38factory = new IphoneFactory(); 39factory.create(); 40} 41} 程序的运行结果如图5.27所示。 图5.27例523运行结果 在例523中,定义Factory工厂接口,并声明create()工厂方法,将创建产品的操作放在了实现该方法的子工厂AndroidFactory类和IphoneFactory类中。由此可见,工厂方法模式是将简单工厂创建对象的职责,分担到子工厂类中,子工厂相互独立,互相不受影响。 工厂方法模式也有局限之处,当面对有复杂的树形结构的产品时,就必须为每个产品创建一个对应的工厂类,当达到一定数量级就会出现类爆炸。 3. 抽象工厂模式 抽象工厂模式(Abstract Factory Pattern)用于意在创建一系列互相关联或互相依赖的对象,如例524所示。 例524TestAbstractFactoryPattern.java 1// 定义Android接口 2interface Android { 3} 4// 定义Iphone接口 5interface Iphone { 6} 7// 定义安卓手机-A类 8class AndroidA implements Android { 9public AndroidA() { 10System.out.println("安卓手机-A被创建!"); 11} 12} 13// 定义安卓手机-B类 14class AndroidB implements Android { 15public AndroidB() { 16System.out.println("安卓手机-B被创建!"); 17} 18} 19// 定义苹果手机-A类 20class IphoneA implements Iphone { 21public IphoneA() { 22System.out.println("苹果手机-A被创建!"); 23} 24} 25// 定义苹果手机-B类 26class IphoneB implements Iphone { 27public IphoneB() { 28System.out.println("苹果手机-B被创建!"); 29} 30} 31// 定义工厂接口 32interface Factory { 33public Android createAndroid(); 34public Iphone createIphone(); 35} 36// 创建型号A的产品工厂 37class FactoryA implements Factory { 38public Android createAndroid() { 39return new AndroidA(); 40} 41public Iphone createIphone() { 42return new IphoneA(); 43} 44} 45// 创建型号B的产品工厂 46class FactoryB implements Factory { 47public Android createAndroid() { 48return new AndroidB(); 49} 50public Iphone createIphone() { 51return new IphoneB(); 52} 53} 54public class TestAbstractFactoryPattern { 55public static void main(String[] args) { 56// 根据不同的型号创建产品 57Factory factory = null; 58factory = new FactoryA();// 创建A工厂 59factory.createAndroid(); // 创建安卓-A手机 60factory.createIphone(); // 创建苹果-A手机 61factory = new FactoryB(); // 创建B工厂 62factory.createAndroid(); // 创建安卓-B手机 63factory.createIphone(); // 创建苹果-B手机 64} 65} 程序的运行结果如图5.28所示。 图5.28例524运行结果 在例524中,定义了Factory工厂接口,并声明两个方法,对应创建Android和Iphone两种产品。而FactoryA和FactoryB子工厂实现工厂接口,用于创建一系列产品。由此可见,抽象工厂是在工厂方法基础上进行了分类管理。 5.4.5设计模式——代理设计模式 代理模式(Proxy)是指给某一个对象提供一个代理,并由代理对象控制对原有对象的引用。如生活中,求职者找工作(真实操作),可以让猎头帮忙去找(代理操作),猎头把最终结果反馈给求职者。无论是真实操作还是代理操作,目的都是一样的,求职者只关心最终结果,而不关心过程,如例525所示。 例525TestProxyPattern.java 1// 定义工作接口 2interface Work { 3void find(); 4} 5// 真实操作 6class Real implements Work{ 7public void find() { 8System.out.println("投递简历"); 9} 10} 11// 代理操作 12class Proxy implements Work{ 13private Work work; 14public Proxy(Work work) { 15this.work = work; 16} 17public void find() { 18System.out.println("合法验证"); 19work.find(); 20System.out.println("反馈结果"); 21} 22} 23public class TestProxyPattern { 24public static void main(String[] args) { 25Work work = null; 26work = new Proxy(new Real());// 交给猎头去找 27work.find(); 28} 29} 程序的运行结果如图5.29所示。 图5.29例525运行结果 在例525中,代理类Proxy完成了代理操作,由此可见,代理操作最终还是调用了真实操作。 5.5包 当声明的类很多时,类名就有可能冲突,这时就需要一种机制来管理类名,因此,Java中引入了包机制,本节将详细介绍包的用法。 5.5.1包的定义与使用 包(package)是Java提供的一种区别类的名字空间的机制,是类的组织方式,是一组相关类和接口的集合,它提供了访问权限和命名的管理机制。 使用package语句声明包,其语法格式如下。 package 包名 使用时需要注意以下四点。 (1) 包名中字母一般都要小写。 (2) 包的命名规则: 将公司域名反转作为包名。 (3) package语句必须是程序代码中的第一行可执行代码。 (4) package语句最多只有一句。 包与文件目录类似,可以分成多级,多级之间用“.”符号进行分隔,具体示例如下。 package com.l000phone.www; 如果在程序中已声明了包,就必须将编译生成的字节码文件保存到与包名同名的子目录中,可以使用带包编译命令,具体示例如下。 javac -d . Source.java 其中,“d”表示生成以package定义为准的目录,“.”表示在当前所在的文件夹中生成。编译器会自动在当前目录下建立与包名同名的子目录,并将生成的.class文件自动保存到与包名同名的子目录下。 接下来分步骤讲解包机制在cmd中通过命令提示符管理Java源文件。 (1) 在源文件首行声明包,如例526所示。 例526TestPackage.java 1// 声明类在com.l000phone.www包下 2package com.l000phone.www; 3public class TestPackage { 4public static void main(String[] args) { 5System.out.println("包机制"); 6} 7} (2) 使用“javac d . TestPackage.java”编译源文件,程序的运行结果如图5.30所示。 图5.30编译源文件 (3) 执行命令完成后,在当前目录下会生成包名“com.1000phone.www”对应的目录,如图5.31所示。 图5.31包编译 (4) 使用“java com.1000phone.www.TestPackage”命令运行程序,运行带包名的字节码文件时,必须输入完整的“包.类名称”。程序的运行结果如图5.32所示。 图5.32例526运行结果 由此可见,包的管理机制让类管理更加方便。 5.5.2import语句 在实际开发中,项目都是分模块开发的,对应模块包中的类,完成相应模块的功能。但有时模块之间的类要相互调用。例如,通常开发中都是将业务逻辑层的接口和实现放在不同包中,在Eclipse中新建一个chapter05项目,在项目的src根目录下新建service和service.impl两个包,在service包中创建UserService接口,在service.impl包下创建UserServiceImpl类,如例527和例528所示。 例527UserService.java 1package service; 2public interface UserService { 3public void say(); 4} 例528UserServiceImpl.java 1package service.impl; 2public class UserServiceImpl implements UserService { 3public void say() { 4System.out.println("用户信息"); 5} 6// 测试 7public static void main(String[] args) { 8UserService user = new UserServiceImpl(); 9user.say(); 10} 11} 使用命令“javac d.IUserService.java”和“javac d.UserServiceImpl.java”编译源文件,程序的运行结果如图5.33所示。 图5.33编译错误 在图5.33中,程序编译结果报错并提示“UserService cannot be resolved to a type”。这是因为类UserService位于service包中,而类UserServiceImpl位于service.impl包中,两者虽然是父包和子包的逻辑关系,但在用法上则不存在任何关系,如果要使用包中的类,则必须import该类所在的包。修改例528的代码,修改后的代码如下。 package service.impl; import service.UserService; public class UserServiceImpl implements UserService { public void say() { System.out.println("用户信息"); } // 测试 public static void main(String[] args) { UserService user = new UserServiceImpl(); user.say(); } } 重新编译UserServiceImpl类,输入命令并运行UserServiceImpl类,命令为“java com.1000phone.www.javaTrain.user.service.impl.UserServiceImpl”,程序的运行结果如图5.34所示。 图5.34例528修改后运行结果 注意: import关键字用于导入指定包层次下的某个类或全部类,import语句应放在package语句之后,类定义之前,其语法格式如下。 import 包名.类名// 导入单类 import 包名.* // 导入包层次下的全部类 接下来在service.impl包下新建UserServiceImp102类,其内容是对例528中的修改,修改后的代码如例529所示。 例529UserServiceImp102.java 1package service.impl; 2import service.*; 3public class UserServiceImpl02 implements UserService { 4public void say() { 5System.out.println("用户信息"); 6} 7// 测试 8public static void main(String[] args) { 9UserService user = new UserServiceImpl02(); 10user.say(); 11} 12} 程序的运行结果如图5.35所示。 图5.35例529运行结果 在例529中,使用import关键字导入指定包层次下的全部类。因此,无须再使用包名.类名的方式。 在JDK 5.0后提供了静态导入功能,用于导入指定类的某个静态成员变量、方法或全部的静态成员变量、方法,具体示例如下。 import static 包名.类名.成员// 导入类指定静态成员 import static 包名.类名.*// 导入该类全部静态成员 import语句和import static语句之间没有任何顺序要求。使用import导入包后,可以在代码中直接访问包中的类,即可以省略包名; 而使用import static导入类后,可以在代码中直接访问类中的静态成员,即可以省略类名。接下来通过代码进行演示,在chapter05项目根目录下新建util包,并在该包下创建Calc类,然后在项目根目录下再新建test包,并在该包下创建TestImportStatic类,如例530和例531所示。 例530Calc.java 1package util; 2public class Calc { 3public static int add(int a, int b) { 4return a+b; 5} 6public static int sub(int a, int b) { 7return a-b; 8} 9public static int mul(int a, int b) { 10return a*b; 11} 12public static int div(int a, int b) { 13return a/b; 14} 15} 例531TestImportStatic.java 1package test; 2import static util.Calc.*; 3public class TestImportStatic { 4public static void main(String[] args) { 5// 省略类名直接调用静态方法 6System.out.println("10+2="+add(10, 2)); 7System.out.println("10-2="+sub(10, 2)); 8System.out.println("10*2="+mul(10, 2)); 9System.out.println("10/2="+div(10, 2)); 10} 11} 程序的运行结果如图5.36所示。 图5.36例531运行结果 在例531中,通过import static导入了Calc类中的全部静态方法,而在使用时省略了类名。由此可见,import和import static功能相似,只是导入的对象不一样,都是用于减少代码量。 5.5.3Java的常用包 Java的核心类都放在java包及其子包下,Java扩展的类都放在javax包及其子包下,接下来了解一下常用的开发包,如表5.3所示。 表5.3Java系统包 包名功 能 描 述 java.lang核心包,如String、Math、System类等,无须使用import手动导入,系统自动导入 java.util工具包,包含工具类、集合类等,如Arrays、List和Set等 java.net包含网络编程的类和接口 java.io包含输入、输出编程相关的类和接口 java.text包含格式化相关的类 java.sql数据库操作包,提供了各种数据库操作的类和接口 java.awt包含抽象窗口工具集(Abstract Window Toolkits,AWT)相关类和接口,主要用于构建图形用户界面(GUI) java.swing包含图形用户界面相关类和接口 5.5.4给Java应用程序打包 在实际开发中,通常会将一些类提供给别人使用,直接提供字节码文件会比较麻烦,所以一般会将这些类文件打包成jar文件,以供别人使用。jar文件的全称是Java Archive File,意思就是Java归档文件,也称为jar包。将一个jar包添加到classpath环境变量中,Java虚拟机会自动解压jar包,根据包名所对应的目录结构去查找所需的类。 通常使用jar命令来打包,可以把一个或多个路径压缩成一个jar文件。jar命令在JDK安装目录下的bin目录中,直接在命令行中输入jar命令,即可查看jar命令的提示信息,如图5.37所示。 图5.37jar命令 jar命令主要参数如下。 (1) c: 创建新的文档。 (2) v: 生成详细的输出信息。 (3) f: 指定归档的文件名。 下面为一个独立的类进行类打包,如例532所示。 例532User.java 1package com.l000phone.www.javaTrain.user.domain; 2public class User { 3private String name; 4public User(String name) { 5this.name = name; 6} 7public String toString() { 8return "User [name=" + name + "]"; 9} 10} 输入命令“javac d . User.java”编译程序,生成目录如图5.38所示。 图5.38生成目录 接下来分步骤学习如何用jar命令进行文件打包及运用jar包。 (1) 打开命令行窗口,切换到将要被打包目录的上级目录,输入命令: jar -cvf common.jar com 上面的命令是将com目录下的全部内容打包成common.jar文件,如果硬盘上有同名jar将被覆盖,运行结果如图5.39和图5.40所示。 图5.39压缩jar过程信息 图5.40生成的jar (2) 输入“set classpath=.; D:\com\1000phone\chapter05\05\common.jar”命令,将jar包添加到环境变量中,如图5.41所示。 图5.41添加环境变量 (3) 删除生成的包目录及User.class文件。编写测试类,测试jar包是否可用,如例533所示。 例533TestImportJar.java 1package com.l000phone.www.javaTrain.test; 2import com.l000phone.www.javaTrain.user.domain.*; 3 4public class TestImportJar{ 5public static void main(String[] args) { 6User user = new User("张三"); 7System.out.println(user); 8} 9} 程序的运行结果如图5.42所示。 图5.42例533运行结果 (4) 查看jar包命令如下。 jar -tvf jar文件名 如查看common.jar文件,则输入命令“jar tvf common.jar”,运行结果如图5.43所示。 图5.43查看jar 在图5.43中,可发现jar包中存在METAINF文件夹,在此文件夹中存在名为MANIFEST.MF的文件,该文件就是jar文件的清单文件。 (5) 解压jar包命令如下。 jar -xvf jar文件名 如解压common.jar文件,则输入命令“jar xvf common.jar”,参数x代表从归档中提取指定的文件。命令将jar包解压到当前目录,运行结果如图5.44和图5.45所示。 图5.44解压命令 图5.45解压后的jar文件 5.6Lambda表达式 Lambda表达式是Java 8发布的重要新特性,可以把它理解为一段能够像数据一样进行传递的代码,它允许把函数作为参数传递给其他的方法。在开发的过程中使用Lambda表达式可以使代码更加简洁,可读性更强。 5.6.1Lambda表达式语法 Java 8中引入了一个新的操作符,“>”可以称为箭头操作符或者Lambda操作符。当使用Lambda表达式进行代码编写时就需要使用这个操作符。箭头操作符将Lambda表达式分成左右两部分,在操作符的左侧代表着Lambda表达式的参数列表(接口中抽象方法的参数列表),在操作符的右侧代表着Lambda表达式中所需执行的功能(是对抽象方法的具体实现)。Lambda表达式的语法格式如下。 (parameters) -> expression或(parameters) ->{statements; } 上述语法还可以写成以下几种格式。 无参数无返回值: ()>具体实现。 有一个参数无返回值: (x)>具体实现,或x>具体实现。 有多个参数,有返回值,并且Lambda体中有多条语句: (x,y)>{具体实现}。 若方法体只有一条语句,那么大括号和return都可以省略。 注意: Lambda表达式的参数列表的参数类型可以省略不写,可以进行类型推断。在Java 8之后可以使用Lambda表达式来表示接口的一个实现,在Java 8之前通常是使用匿名类实现的。 5.6.2Lambda表达式案例 接下来通过代码讲解Lambda表达式的使用,编写一个能够实现加、减、乘、除功能且能够实现输出字符串功能的案例。首先在chapter05项目的根目录下创建lambda包,在该包下创建MathOne接口,该接口中定义一个带有两个参数的operation方法,代码如例534所示。 例534MathOne.java 1package lambda; 2//定义接口MathOne 3public interface MathOne { 4//定义一个方法operation 5int operation(int a, int b); 6} 然后,在lambda包下创建ServiceOne接口,在该接口中定义含有一个参数的printMessage方法,具体代码如例535所示。 例535ServiceOne.java 1package lambda; 2//定义接口ServiceOne 3public interface ServiceOne { 4//定义一个printMessage方法 5void printMessage(String message); 6} 最后,在lambda包下创建测试类TestLam,在该类中实现MathOne、ServiceOne接口,编写功能代码,具体代码如例536所示。 例536TestLam.java 1package lambda; 2public class TestLam { 3public static void main(String[] args) { 4TestLam testlam = new TestLam(); 5//实现MathOne接口,做加法运算,有参数类型 6MathOne jiafa = (int a,int b)->a+b; 7//实现MathOne接口,做减法运算,无参数类型 8MathOne jianfa = (a,b)->b-a; 9//实现MathOne接口,做乘法运算,方法体外有大括号及返回值语句 10MathOne chengfa = (int a,int b)->{return a*b;}; 11//实现MathOne接口,做除法运算,方法体外无大括号及返回值 12MathOne chufa= (int a,int b)->b/a; 13//实现ServiceOne接口,控制台打印 14ServiceOne service = 15(message)->System.out.println("Hello,"+message); 16service.printMessage("这是Java 8的新特性"); 17service.printMessage("这是一个Lambda表达式"); 18System.out.println("2+4="+testlam.operate(2,4,jiafa)); 19System.out.println("4-2="+testlam.operate(2,4,jianfa)); 20System.out.println("2*4="+testlam.operate(2,4,chengfa)); 21System.out.println("4/2="+testlam.operate(2,4,chufa)); 22} 23private int operate(int a,int b,MathOne mathOne){ 24return mathOne.operation(a,b); 25} 26} 运行以上代码,可以发现控制台打印的结果中实现了加、减、乘、除和打印字符串的功能,运行结果如图5.46所示。 图5.46例536运行结果 5.6.3函数式接口 1. @FunctionalInterface注解 Java 8为函数式接口引入了一个新注解@FunctionalInterface,主要用于编译级错误检查,加上该注解,当编写的接口不符合函数式接口定义的时候,编译器就会报错。函数式接口(Functional Interface)是指仅包含一个抽象方法,但是可以包含多个非抽象方法(比如,静态方法、默认方法)的接口。 正确案例: package chapter05; @FunctionalInterface public interface ServiceOne { //抽象方法printMessage void printMessage(String message); } 错误案例: package chapter05; @FunctionalInterface public interface ServiceOne { //抽象方法printMessage void printMessage(String message); //抽象方法printMessage2 void printMessage2(String message); } 以上的错误案例中有两个抽象方法,一个是printMessage(),一个是printMessage2(); 这与函数式接口定义有冲突,因此编译器报错。 函数式接口里是可以包含默认方法的,因为默认方法不是抽象方法,有一个默认的实现,所以是符合函数式接口的定义的,如下代码中不仅含有一个抽象方法,还有一个默认方法,代码编译时不会报错。 package chapter05; @FunctionalInterface public interface ServiceOne { //抽象方法 void printMessage(String message); //默认方法 default void printMessage2(String message) { //方法体 }; } 注意: @FunctionalInterface注解加或不加对于接口是不是函数式接口没有任何影响,该注解只是提醒编译器去检查该接口是否仅包含一个抽象方法。 函数式接口可以被隐式转换为Lambda表达式。例53中定义的MathOne接口中只有一个operation方法,通常称这种只有一个抽象方法的接口为函数式接口,可以在该接口上添加@FunctionalInterface注解,Lambda表达式需要函数式接口的支持。 2. JDK提供的函数式接口 函数式接口可以对现有的函数友好地支持Lambda,JDK 1.8之前已有很多函数式接口,JDK 1.8新增加的函数接口中包含很多类,用来支持Java的函数式编程,新增的java.util.function包中的函数式接口如表5.4所示。 表5.4JDK 1.8新增加的函数接口 接口描述 BiConsumer<T,U>代表了一个接受两个输入参数的操作,并且不返回任何结果 BiFunction<T,U,R>代表了一个接受两个输入参数的方法,并且返回一个结果 BinaryOperator<T>代表了一个作用于两个同类型操作符的操作,并且返回了操作符同类型的结果 BiPredicate<T,U>代表了一个两个参数的boolean值方法 BooleanSupplier代表了boolean值结果的提供方 Consumer<T>代表了接受一个输入参数并且无返回的操作 DoubleBinaryOperator代表了作用于两个double值操作符的操作,并且返回了一个double值的结果 DoubleConsumer代表一个接受double值参数的操作,并且不返回结果 DoubleFunction<R>代表接受一个double值参数的方法,并且返回结果 DoublePredicate代表一个拥有double值参数的boolean值方法 DoubleSupplier代表一个double值结构的提供方 DoubleToIntFunction接受一个double类型输入,返回一个int类型结果 DoubleToLongFunction接受一个double类型输入,返回一个long类型结果 DoubleUnaryOperator接受一个参数同为类型double,返回值类型也为double Function<T,R>接受一个输入参数,返回一个结果 IntBinaryOperator接受两个参数同为类型int,返回值类型也为int IntConsumer接受一个int类型的输入参数,无返回值 IntFunction<R>接受一个int类型输入参数,返回一个结果 IntPredicate接受一个int类型输入参数,返回一个布尔值的结果 IntSupplier无参数,返回一个int类型结果 IntToDoubleFunction接受一个int类型输入,返回一个double类型结果 IntToLongFunction接受一个int类型输入,返回一个long类型结果 IntUnaryOperator接受一个参数同为类型int,返回值类型也为int LongBinaryOperator接受两个参数同为类型long,返回值类型也为long LongConsumer接受一个long类型的输入参数,无返回值 LongFunction<R>接受一个long类型的输入参数,返回一个结果 LongPredicate接受一个long类型的输入参数,返回一个布尔值类型结果 LongSupplier无参数,返回一个long类型的结果 LongToDoubleFunction接受一个long类型输入,返回一个double类型结果 LongToIntFunction接受一个long类型输入,返回一个int类型结果 LongUnaryOperator接受一个参数同为类型long,返回值类型也为long ObjDoubleConsumer<T>接受一个object类型和一个double类型的输入参数,无返回值 ObjIntConsumer<T>接受一个object类型和一个int类型的输入参数,无返回值 ObjLongConsumer<T>接受一个object类型和一个long类型的输入参数,无返回值 Predicate<T>接受一个输入参数,返回一个布尔值结果 Supplier<T>无参数,返回一个结果 ToDoubleBiFunction<T,U>接受两个输入参数,返回一个double类型结果 ToDoubleFunction<T>接受一个输入参数,返回一个double类型结果 ToIntBiFunction<T,U>接受两个输入参数,返回一个int类型结果 ToIntFunction<T>接受一个输入参数,返回一个int类型结果 ToLongBiFunction<T,U>接受两个输入参数,返回一个long类型结果 ToLongFunction<T>接受一个输入参数,返回一个long类型结果 UnaryOperator<T>接受一个参数为类型T,返回值类型也为T 3. 函数式接口实例 接下来以Predicate<T>接口为例,讲解函数式接口的使用方法,实现对集合中的元素按条件进行筛选的功能。首先创建Test2类,并在该类中定义一个List集合。由表5.4可知,Predicate<T>接口接受一个参数,返回一个布尔值结果。接下来演示函数式接口,具体代码如例537所示。 例537Test2.java 1package chapter05; 2import java.util.Arrays; 3import java.util.List; 4import java.util.function.Predicate; 5public class Test2 { 6public static void main(String args[]){ 7// 定义一个list集合 8List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 99,10); 10// Predicate<Integer> predicate = n -> true 11// n 是一个参数传递到 Predicate 接口的 test 方法 12// n 如果存在则 test 方法返回 true 13System.out.println("输出集合中的所有数据:"); 14// 传递参数 n 15testA(list, n->true); 16// Predicate<Integer> predicate1 = n -> n%2 == 0 17// n 是一个参数传递到 Predicate 接口的 test 方法 18// 如果 n%2 为 0 ,test 方法返回 true 19System.out.println(); 20System.out.println("输出集合中所有大于4的偶数:"); 21testA(list, n-> n%2 == 0 && n>4); 22// Predicate<Integer> predicate2 = n -> n > 3 23// n 是一个参数传递到 Predicate 接口的 test 方法 24// 如果 n 大于 3 ,test方法返回 true 25System.out.println(); 26System.out.println("输出集合中大于 6 的所有数字:"); 27testA(list, n-> n > 6); 28} 29//定义testA方法 30public static void testA(List<Integer> list, Predicate<Integer> 31predicate) { 32//遍历集合中的元素 33for(Integer n: list) { 34//满足条件的打印 35if(predicate.test(n)) { 36System.out.print(n + " "); 37} 38} 39} 40} 运行以上代码,运行结果如图5.47所示。 图5.47例537运行结果 5.6.4方法引用与构造器引用 当要传递给Lambda体的操作已经有实现的方法了,可以使用方法引用,可以理解为方法引用是Lambda表达式的另外一种表现形式。实现抽象方法的参数列表,必须与引用方法的参数列表保持一致。方法引用的语法: 对象∷实例方法,类∷静态方法,类∷实例方法。构造器引用的语法: ClassName∷new。 第一种语法: 对象∷实例方法名。 public void TestOne() { Consumer<String> con = (s) -> System.out.println(s); Consumer<String> consumer = System.out::println; consumer.accept("HelloWorld!"); } 第二种语法: 类∷静态方法名。Lambda体中调用方法的参数列表与返回值类型要与函数式接口中抽象方法的函数列表与返回值类型保持一致。 public void TestTwo() { Comparator<Integer> comparator = (m,n) -> Integer.compare(m,n); Comparator<Integer> com = Integer::compare; } public void TestTwo() { Comparator<Integer> comparator = (m,n) -> Integer.compare(m,n); Comparator<Integer> com = Integer::compare; } 第三种语法: 类∷实例方法名。如果第一个参数是调用者,第二个参数是被调用者,则可以使用这种方式。 public void TestThree() { BiPredicate<String,String> bip = (x,y) -> x.equals(y); BiPredicate<String,String> bp = String::equals; } 在了解了方法引用与构造器引用的书写格式后,下面通过案例在Animal类中定义了4个方法作为例子来区分Java中4种不同方法的引用,具体代码如例538所示。 例538Animal.java 1package chapter05; 2import java.util.Arrays; 3import java.util.List; 4class Animal { 5@FunctionalInterface 6public interface Supplier<T> { 7T get(); 8} 9//Supplier是JDK 1.8提供的接口,这里和Lambda一起使用了 10public static Animal create(final Supplier<Animal> supplier) 11{ 12return supplier.get(); 13} 14public static void collide(final Animal animal) { 15System.out.println("Collided " + animal.toString()); 16} 17public void follow(final Animal another) { 18System.out.println("Following " + another.toString()); 19} 20public void repair() { 21System.out.println("Repaired " + this.toString()); 22} 23public static void main(String[] args) { 24//构造器引用:它的语法是Class::new,或者更一般的Class< T >::new,实例如下: 25Animal animal= Animal.create(Animal::new); 26Animal dog = Animal.create(Animal::new); 27Animal pig = Animal.create(Animal::new); 28Animal bear = new Animal(); 29List<Animal> animals = Arrays.asList(animal,dog,pig,bear); 30System.out.println("===================构造器引用 31========================"); 32//静态方法引用:它的语法是Class::static_method,实例如下: 33animals.forEach(Animal::collide); 34System.out.println("===================静态方法引用 35 ========================"); 36//特定类的任意对象的方法引用:它的语法是Class::method,实例如下: 37animals.forEach(Animal::repair); 38System.out.println("==============特定类的任意对象的方法引用 39 ================"); 40//特定对象的方法引用:它的语法是instance::method,实例如下: 41final Animal duixiang = Animal.create(Animal::new); 42animals.forEach(duixiang::follow); 43System.out.println("===================特定对象的方法引用 44==================="); 45} 46} 运行以上代码,结果如图5.48所示。 图5.48例538运行结果 小结 通过本章的学习,读者能够掌握Java面向对象的另外两大特征: 继承和多态,了解Java 8推出的新特性Lambda表达式。重点要理解的是继承可以让某个类型的对象获得另一个类型的对象的属性的方法,而多态就是指一个类实例的相同方法在不同情形有不同表现形式。 习题 1. 填空题 (1) 如果在子类中需要访问父类的被重写方法,可以通过关键字来实现。 (2) Java中使用关键字来表示抽象的意思。 (3) Java中使用关键字来实现接口的继承。 (4) 工厂模式主要有三种形态: 简单工厂模式、工厂方法模式和模式。 (5) Java 8中引入了一个新的操作符,“>”可以称为箭头操作符或者操作符。 2. 选择题 (1) 以下关于Java语言继承的说法正确的是()。 A. Java中的类可以有多个直接父类B. 抽象类不能有子类 C. Java中的接口支持多继承D. 最终类可以作为其他类的父类 (2) 现有两个类A、B,以下描述中表示B继承自A的是()。 A. class A extends BB. class B implements A C. class A implements BD. class B extends A (3) 下列选项中,用于定义接口的关键字是()。 A. interfaceB.implementsC. abstractD. class (4) 下列选项中,表示数据或方法只能被本类访问的修饰符是()。 A. publicB. protectedC. privateD. final (5) 在Java中,关于@FunctionalInterface注解代表的含义,下列说法正确的是()。 A. 主要用于编译级错误检查 B. 主要用于简化Java开发的工具注解 C. 检查是否含有多个非抽象方法 D. 当接口中包含默认方法时代码编译会报错 3. 思考题 (1) 请简述什么是继承。 (2) 请简述什么是多态,什么是动态绑定。 (3) 请简述方法的重载与重写的区别。 (4) 请简述抽象类和接口的区别。 4. 编程题 (1) 设计一个名为Geometric的几何图形的抽象类,该类包括: ① 两个名为color、filled的属性分别表示图形颜色和是否填充。 ② 一个无参的构造方法。 ③ 一个能创建指定颜色和填充值的构造方法。 ④ 一个名为getArea()的抽象方法,返回图形的面积。 ⑤ 一个名为getPerimeter()的抽象方法,返回图形的周长。 ⑥ 一个名为toString()的方法,返回圆的字符串描述。 (2) 设计一个名为Circle的圆类来实现Geometric类,该类包括: ① 一个名为radius的double属性表示半径。 ② 一个无参构造方法创建圆。 ③ 一个能创建指定radius的圆的构造方法。 ④ radius的访问器方法。 ⑤ 一个名为getArea()的方法,返回该圆的面积。 ⑥ 一个名为getPerimeter()的方法,返回圆的周长。 ⑦ 一个名为toString()的方法,返回该圆的字符串描述。 (3) 设计一个名为Rectangle的矩形类来实现Geometric类,该类包括: ① 两个名为side1、side2的double属性表示矩形的两条边。 ② 一个无参构造方法创建矩形。 ③ 一个能创建指定side1和side2的圆的构造方法。 ④ side1和side2的访问器方法。 ⑤ 一个名为getArea()的方法,返回该矩形的面积。 ⑥ 一个名为getPerimeter()的方法,返回该矩形的周长。 ⑦ 一个名为toString()的方法,返回该矩形的字符串描述。 (4) 设计一个名为Triangle的三角形类来实现Geometric类,该类包括: ① 三个名为side1、side2和side3的double属性表示三角形的三条边。 ② 一个无参构造方法创建三角形。 ③ 一个能创建指定side1、side2和side3的矩形的构造方法。 ④ side1、side2和side3的访问器方法。 ⑤ 一个名为getArea()的方法,返回该三角形的面积。 ⑥ 一个名为getPerimeter()的方法,返回该三角形的周长。 ⑦ 一个名为isTriangle()的方法,判断三边是否能构成三角形。 ⑧ 一个名为toString()的方法,返回三边较小的字符串描述。 (5) 编写测试类,测试图形的面积和周长。