本章将详细介绍接口的相关知识点,多态所具有的特性及引用传递在Java中的运用,可变参数的使用及Object类,泛型等。
9.1 接口
上一章介绍了抽象类,知道抽象类必须要有子类才能被实例化。而抽象类中可以有抽象方法也可以有非抽象方法。而当抽象类中所有方法都是抽象方法时,此时该类就可以用接口(interface)来定义。
接口是Java所提供的另一种重要技术,它的结构和抽象类非常相似,也具有成员变量和抽象方法。但它又和抽象类有所不同。
1. 接口中所定义的成员变量都是公共的静态常量,并且必须赋初值;
2. 接口中定义的方法都是公共的抽象方法。
接口定义的语法格式如下:
interface 接口名称{
[public][static][final] 数据类型 成员变量名称 = 初始值;
[public][abstract] 返回值类型 方法名称(参数列表);
}
当定义成员变量前面没有定义public static final这三个关键词时,Java虚拟机在编译时会自动把这三个关键词加上,一般推荐定义变量时这几个关键词写上;当定义方法前面没有public abstract这两个关键词修饰时,Java虚拟机在编译时会自动把这二个关键词加上,一般推荐public写上,而abstract不写。
以打印机为例来说明接口的使用。对于打印机来说,需要根据墨盒和纸张来决定是打印彩色的还是黑白的,是打印A4的还是打印A5的。首先定义墨盒接口如下:
范例1:InkBox.java
public interface InkBox{ public abstract String getColor(); }
然后再定义纸张接口:
范例2:Paper.java
public interface Paper{ public abstract String getSize(); }
对于接口而言,它和抽象类是一样的,都不能直接实现化,需要有子类。抽象类是通过extends来继承,而接口需要使用关键词implements来实现。语法格式如下:
class 类名称 implements 接口1,接口2,...,接口n{
实现父类方法代码块;
}
现在假设有彩色和黑白两种颜色的墨盒,以及A4和A5两种类型的纸张,那么需要定义如下几个类来实现上面两个接口。
范例3:A4Paper.java
public class A4Paper implements Paper{ public String getSize(){ return "A4"; } }
范例4:A5Paper.java
public class A5Paper implements Paper{ public String getSize(){ return "A5"; } }
范例5:ColorInkBox.java
public class ColorInkBox implements InkBox{ public String getColor(){ return "彩色"; } }
范例6:GrayInkBox.java
public class GrayInkBox implements InkBox{ public String getColor(){ return "黑白"; } }
在Java中,接口可以多实现,也就是说一个类可以同时实现多个接口。
范例7:InterfaceDemo01.java
interface InterA{ public void show(); } interface InterB{ public void method(); } class InterC implements InterA,InterB{ public void show(){ System.out.println("show()...."); } public void method(){ System.out.println("method()...."); } } public class InterfaceDemo01{ public static void main(String[] args){ InterC interC = new InterC(); interC.show(); interC.method(); } }
在Java中,接口也可以继承接口,并且还支持实现多继承。
范例8:InterfaceDemo02.java
interface InterA{ public void show(); } interface InterB{ public void method(); } interface InterC extends InterA,InterB{ public void function(); } class InterD implements InterC{ public void show(){ System.out.println("show()...."); } public void method(){ System.out.println("method()...."); } public void function(){ System.out.println("function()...."); } } public class InterfaceDemo02{ public static void main(String[] args){ InterD interD = new InterD(); interD.show(); interD.method(); interD.function(); } }
注:当子类实现接口后,必须重写接口中所有方法,如果子类没有实现接口中的方法,那么该子类必须是抽象类或接口。
对于接口的多继承需要注意的是,父类中的方法不能有相同名称的方法。比如:
范例8:InterfaceDemo03.java
interface InterA{ public void show(); public int method(); } interface InterB{ public void method(); } interface InterfaceDemo03 extends InterA,InterB{ public void function(); }
程序编译时报如下错误信息:
InterfaceDemo03.java:8: 错误: 类型InterB和InterA不兼容; 两者都定义了method(), 但却带有不相关的返回类型
interface InterfaceDemo03 extends InterA,InterB{
^
1 个错误
这个错误信息说明父类中如果有相同名称的方法,那么其返回值类型也必须一至。
9.2 多态
9.2.1 多态的基本概念
前面章节已经详细介绍了面向对象的二大特性:封装和继承,本节来说下第三大特性——多态。
那么什么是多态呢?
同一个对象的引用,在不同的实例下实现不同的功能就叫多态。对于程序而言表现最为常见的就是父类引用指向子类对象就是多态。
要实现多态,必须具备两个前提:其一是类与类之间必须要先建立关系——继承或实现;其二是子类必须要重写父类中的方法。
9.2.2 如何多态
还是以打印机的例子来说明多态在程序开发中的运用。现编写打印机类,由于打印机需要使用墨盒和纸张,所以在打印机类中引用了两个接口对象作为变量,并提供它们的set方法,以方便进行初始化操作。而打印机的核心功能就是打印信息,因此在打印机类中提供一个打印方法。示例代码如下:
范例9:Printer.java
public class Printer{ private InkBox inkBox; //墨盒 private Paper paper; //纸张 public void setInkBox(InkBox inkBox){ this.inkBox = inkBox; } public void setPaper(Paper paper){ this.paper = paper; } public void print(){ System.out.println("使用" + inkBox.getColor() + "墨盒在" + paper.getSize() + "纸张上打印。"); } }
编写好打印机类后,就可以编写测试类来测试打印机是否能正常工作了。
范例10:PrinterTest.java
public class PrinterTest{ public static void main(String[] args){ //实例化墨盒 InkBox color = new ColorInkBox(); InkBox gray = new GrayInkBox(); //实例化纸张 Paper a4 = new A4Paper(); Paper a5 = new A5Paper(); //实例化打印机 Printer printer = new Printer(); printer.setInkBox(color); printer.setPaper(a4); printer.print(); System.out.println("-------------"); printer.setInkBox(gray); printer.setPaper(a5); printer.print(); } }
程序运行结果为:
使用彩色墨盒在A4纸张上打印。
-------------
使用黑白墨盒在A5纸张上打印。
在上例中,实例化墨盒和实例化纸张都用到了多态。在第4行代码中,把墨盒对象(inkBox)指向到了彩色墨盒对象(ColorInkBox),这就是多态的一种表现。多态的另一种表现就是以参数的方式进行传递。看下面示例:
范例11:PrinterTest01.java
public class PrinterTest01{ public static void main(String[] args){ Printer printer = new Printer(); printer.setInkBox(new ColorInkBox()); printer.setPaper(new A5Paper()); printer.print(); } }
本例就是使用参数的方式来实现多态——用父类来接收子类对象。
9.2.3 多态的弊端
在多态中,如果子类没有重写父类中的方法,那么父类会隐藏子类特有的方法。
范例12:PolymorphismDemo01.java
class Person{ public void fun(){ System.out.println("fun..."); } } class Student extends Person{ public void sleep(){ System.out.println("sleep..."); } } public class PolymorphismDemo01{ public static void main(String[] args){ Person p = new Student(); p.sleep(); } }
程序编译时提示错误信息如下:
PolymorphismDemo01.java:12: 错误: 找不到符号
p.sleep();
^
符号: 方法 sleep()
位置: 类型为Person的变量 p
1 个错误
这个提示信息说明Person对象中没有找到sleep()方法。那么为什么不能运行sleep()方法呢?因为执行sleep()方法的对象是p,它指向的是Person,而Person中并没有sleep()方法,该方法是其子类中特有的方法,所以报找不到的错误。
注:多态会隐藏子类特有的方法。
9.2.4 类型转换
在Java中类型转换有两种:一种是向上类型转换,一种是向下类型转换。
1. 向上类型转换
所谓向上类型转换就是子类对象提升为父类对象,这种转换是程序自动完成的。比如Person p = new Student();,此时Student对象就自动提升为Person对象。
2. 向下类型转换
向下类型转换是把父类对象转换为子类对象,这种转换需要进行强制类型转换。
范例13:PolymorphismDemo02.java
public class PolymorphismDemo02{ public static void main(String[] args){ Person p = new Student(); Student s = (Student)p; s.sleep(); } }
本例首先进行了向上类型转换,然后再通过Student s = (Student)p;把p对象向下转换为Student对象,这就是向下类型转换。
9.2.5 instanceof关键词
程序在进行向下类型转换时,很容易发生错误,为了减少程序在进行向下类型转换中的错误发生,可以使用instanceof关键词来进行判断。其语法格式为:
对象 instanceof 类或接口
该运算符执行的结果是一个布尔值true或false。值为true表示对象是类或接口的实例,值为false表示对象不是类或接口的实例。
范例14:InstanceOfDemo.java
public class InstanceOfDemo{ public static void main(String[] args){ Person p = new Student(); if(p instanceof Student){ Student s = (Student)p; s.sleep(); } } }
9.3 引用传递
引用传递是Java的核心,引用传递的操作核心是内存地址的传递。
范例:ReferenceDemo01.java
class Demo{ private int count = 5; public void setCount(int count){ this.count = count; } public int getCount(){ return count; } } public class ReferenceDemo01{ public static void main(String[] args){ Demo d1 = new Demo(); d1.setCount(100); fun(d1); System.out.println(d1.getCount()); } public static void fun(Demo d2){ d2.setCount(30); } }
程序输出结果为:30
结构图如下:
范例:ReferenceDemo02.java
public class ReferenceDemo02{ public static void main(String[] args){ String str = "hello"; fun(str); System.out.println(str); } public static void fun(String temp){ temp = "world"; } }
程序输出结果为:hello
结构图如下:
范例:ReferenceDemo03.java
class Demo{ private String str = "hello"; public void setStr(String str){ this.str = str; } public String getStr(){ return str; } } public class ReferenceDemo03{ public static void main(String[] args){ Demo d1 = new Demo(); d1.setStr("world"); fun(d1); System.out.println(d1.getStr()); } public static void fun(Demo d2){ d2.setStr("hi"); } }
程序输出结果为:
结构图如下:
9.4 可变参数
在调用方法是,若方法的参数事先无法确定,那么应该如何处理?
在JDK5之后支持不定长自变量(Variable-length Argument)——可变数组,可以轻松解决上面的问题。
可变数组语法格式为:
类型... 数组名称;
范例:VariableLengthArgument.java
public class VariableLengthArgument{ public static void main(String[] args){ int[] arr = {1,3,5,4,8}; show(arr); } public static void show(int... args){ int sum = 0; for(int i=0; i<args.length; i++){ sum += args[i]; } System.out.println("数组元素之和为:"+sum); } }
对于可变数组而言,必须放在参数列表的最后。
class Operate{ public int add(int...arr, int a){ for(int i=0; i<arr.length; i++){ System.out.println(arr[i]); } } }
程序编译时提示信息如下:
Operate.java:2: 错误: 需要')'
public int add(int...arr, int a){
^
Operate.java:2: 错误: 需要';'
public int add(int...arr, int a){
^
2 个错误
该错误信息表示可变数组需要放到参数列表最后,同一个方法参数列表中,可变数组最多只能有一个。
9.5 增强for循环
增强for循环实际上是for循环的增强版,是从JDK5开始的,目的是为了简化代码的编写。增强for循环只是用于迭代数据,而不能在循环的同时修改数据。语法格式为:
for(类型 循环变量 : 集合或数组){}
范例:ForDemo.java
public class ForDemo{ public static void main(String[] args){ Integer arr = {21,76,24,23}; for(Integer i : arr){ System.out.println(i); } } }
9.6 Object类
在Java中,一个类只能有一个直接父类,如果在定义类时没有使用extends关键词继承任何类,那么这个类会继承java.lang.Object类。换句话说,如果定义类public class Demo{}那就相当于定义了public class Demo extends Object{}因此在Java程序中,任何类的根类一定是java.lang.Object类。也就是说,java.lang.Object类可以接收任何类型的对象。如:
Object obj1 = "jock";
Object obj2 = 127;
上面定义的变量都是合法的。
在Object类中共定义了11个方法,其中有5个方法和线程有关。下面举几个方面为例说明其用法。
9.6.1 toString()方法
在JDK中的Object类中定义toString()方法是输出对象的内存地址。定义如下:
public String toString(){
return getClass().getName()+"@"+Integer.toHexString(hashCode());
}
范例:ToStringDemo01.java
class Dog{ String name; public Dog(String name){ this.name = name; } } public class ToStringDemo01{ public static void main(String[] args){ Dog dog = new Dog("长毛"); System.out.println(dog); } }
程序输出结果为:
Dog@665a9c5d
这个结果显然并不是想要的结果,想要的结果是显示狗狗的名称。因此程序需要重写toString()方法。
范例:ToStringDemo02.java
class Dog{ String name; public Dog(String name){ this.name = name; } public String toString(){ return "name="+name; } } public class ToStringDemo02{ public static void main(String[] args){ Dog dog = new Dog("长毛"); System.out.println(dog); } }
此时程序的输出结果为:
name=长毛
注:当在程序中使用System.out.println()方法是,会自动调用toString()方法。
9.6.2 重写equals()方法
在前面章节介绍过两个对象的比较。对于使用“==”进行比较是判断其地址是否相等,而比较其值是否相等是用的equals()方法。从JDK中源代码可以看出,Object类中的equals()方法定义如下:
public boolean equals(Object obj) { return (this == obj); }
重代码可以看出,equals()方法比较的方式还是使用的“==”来进行判断的。因此,当某些需求需要对两个对象进行比较时,可以重写equals()方法来让其根据需求来进行比较。
范例:Student.java
public class Student{ private String name; private int age; public void setName(String name){ this.name = name; } public void setAge(int age){ this.age = age; } public String getName(){ return name; } public int getAge(){ return age; } public boolean equals(Object other){ if(this == other){ return true; } if(!(other instanceof Student)){ return false; } Student s = (Student)other; if(!this.getName().equals(s.getName()) && this.getAge() != s.getAge()){ return false; } return true; } }
分析:在程序equals()方法中的第一个if语句是用来判断两个比较的对象是否为同一个对象,如果是则返回true,也就是说不用比;如果不是则进入第二个if语句。第二个if语句是判断被比较的对象是不是Student对象的实例,如果不是则返回false表示对象不同,如果返回true则表示被比较对象是Student对象的实例,那么就可以把被比较对象转换为Student对象。第三个if语句是判断两个对象中的name和age属性的值是否相同,如果相同表示同一个对象,如果不相同表示不同对象。
9.7 垃圾机制
创建对象就会占用内存空间,如果程序执行过程中某些对象不再使用,那智能卡这些对象就只会消耗内存,变成了垃圾。
对于不再使用的对象,JVM有垃圾回收机制(Garbage Collection,GC),回收到的垃圾对象所占用的内存空间,会被垃圾回收器释放。那么,那些会被JVM视为垃圾对象呢?简单地说,程序执行过程中,无法通过变量引用的对象,就是GC认定的垃圾对象。
现有类Person定义如下:
public class Person{ } public class PersonTest{ public static void main(String[] args){ Person p1 = new Person(); Person p2 = new Person(); p1 = p2; } }
上面的程序实际意义不大,主要是为了说明垃圾回收机制的执行过程。
当程序执行main()方法内的第一行代码时,会在内存开辟一个空间来存放p1对象;执行到第二行代码时,会在内存再开辟一个空间来存放p2对象;当执行第三行代码时,会将p2的引用地址赋给p1的引用地址,换句话说,对象p1此时指向了对象p2所在的地址。p1和p2共同拥有同一个地址而p1原来的地址就没有任何对象在引用了,那么p1对象原来的地址就会被GC视为垃圾,然后回收掉。
9.8 枚举类
9.8.1 枚举的概念
枚举是在JDK5之后增加的一个重要特性,目的是为了提高程序的安全性。枚举中每个值都是枚举对象的一个实例。枚举的定义语法格式如下:
enum 类名称{
值1,值2,...,值n;
}
最后的封号可以不写。本节将详细介绍枚举的使用。
9.8.2 枚举的使用
范例:EnumDemo01.java
enum Season{ SPRING, SUMMER, AUTUMN, WINNTER; } public class EnumDemo01{ public static void main(String[] args){ Season s = Season.SUMMER; System.out.println(s); } }
本例会输出枚举对象中的SUMMER值,当想要获取枚举对象中的多个值是,可以使用循环。
范例:EnumDemo02.java
public class EnumDemo02{ public static void main(String[] args){ for(Season s : Season.values()){ System.out.println(s); } } }
本例输出结果为:
SPRING
SUMMER
AUTUMN
WINNTER
可以使用枚举中的values()方法来获取所有枚举值。
9.8.3 枚举类Enum
当使用关键词enum来定义类时,实际上该类就是一个枚举类,而枚举类会自动继承java.lang.Enum类。
范例:EnumDemo03.java
public class EnumDemo03{ public static void main(String[] args){ for(Season s : Season.values()){ System.out.println(s.ordinal() + " -> " + s.name()); } } }
程序输出结果为:
0 -> SPRING
1 -> SUMMER
2 -> AUTUMN
3 -> WINNTER
分析:枚举类Enum中提供了ordinal()方法来获取枚举值对象的下标,而name()方法是获取枚举值的名称。
9.8.4 定义属性和方法
既然Enum是一个类,那么就应该可以定义属性和方法。那么如何在枚举中定义属性和方法呢?
范例:EnumDemo04.java
enum Season{ SPRING("春"), SUMMER("夏"), AUTUMN("秋"), WINNTER("冬"); //定义构造方法 private Season(String name){ this.setName(name); } private String name; public void setName(String name){ this.name = name; } public String getName(){ return this.name; } } public class EnumDemo04{ public static void main(String[] args){ for(Season s : Season.values()){ System.out.println(s.ordinal() + " -> " + s.name() + " -> " + s.getName()); } } }
程序输出结果为:
0 -> SPRING -> 春
1 -> SUMMER -> 夏
2 -> AUTUMN -> 秋
3 -> WINNTER -> 冬
注:枚举的所有构造方法都是private(私有的)。因此枚举可以作用单例模式来使用。
9.8.5 实现接口和定义抽象方法
枚举除了可以定义上述方法名,还可以实现接口。但是要注意,一旦枚举实现了接口之后,枚举中的每个对象都必须分别的实现这个接口中提供的抽象方法。
范例:EnumDemo05.java
interface InterSeason{ public String getSeasonInfo(); } enum Season implements InterSeason{ SPRING("春"){ public String getSeasonInfo(){ return this.getName(); } }, SUMMER("夏"){ public String getSeasonInfo(){ return this.getName(); } }, AUTUMN("秋"){ public String getSeasonInfo(){ return this.getName(); } }, WINNTER("冬"){ public String getSeasonInfo(){ return this.getName(); } }; //定义构造方法 private Season(String name){ this.setName(name); } private String name; public void setName(String name){ this.name = name; } public String getName(){ return this.name; } } public class EnumDemo05{ public static void main(String[] args){ for(Season s : Season.values()){ System.out.println(s.ordinal() + " -> " + s.name() + " -> " + s.getName()); } } }
本例输出结果同上例。
在枚举中还可以定义抽象方法,和实现接口一样,都需要为第一个枚举对象实现该方法。
范例:EnumDemo06.java
enum Season{ SPRING("春"){ public String getSeasonInfo(){ return this.getName(); } }, SUMMER("夏"){ public String getSeasonInfo(){ return this.getName(); } }, AUTUMN("秋"){ public String getSeasonInfo(){ return this.getName(); } }, WINNTER("冬"){ public String getSeasonInfo(){ return this.getName(); } }; //定义构造方法 private Season(String name){ this.setName(name); } private String name; public void setName(String name){ this.name = name; } public String getName(){ return this.name; } public abstract String getSeasonInfo(); } public class EnumDemo06{ public static void main(String[] args){ for(Season s : Season.values()){ System.out.println(s.ordinal() + " -> " + s.name() + " -> " + s.getName()); } } }
9.9 泛型
JDK5之后还增加了泛型,目的是为了提高程序的安全性。
9.9.1 泛型的作用
泛型的主要目的是为了解决在进行类型转换过程中发生的类型转换异常问题,用于处理安全隐患。
在设置泛型时是通过“<T>”的形式设置的,这里只设置了一个标记,会根据具体的类型来定。为了保证代码能正常使用,在泛型中也可以不设置类型,如果不设置称为擦除泛型,将使用Object来接收。
9.9.2 通配符
范例:GenericDemo01.java
class Util<T>{ private String str; public void setStr(String str){ this.str = str; } public String getStr(){ return str; } } public class GenericDemo01{ public static void main(String[] args){ Util<String> util = new Util<String>(); util.setStr("jock"); fun(util); } public static void fun(Util<String> tmp){ System.out.println(tmp.getStr()); } }
对于fun()方法来说,为了让该方法通过性高一些,可以使用通配符来设置参数。所有的数字的包装类,都是Number的子类,所以这种条件下泛型只能设置Number或Number的子类,因此就可以使用? extends 类的方式来设置泛型参数:
范例:GenericDemo02.java
class Util<T extends Number>{ private T str; public void setStr(T str){ this.str = str; } public T getStr(){ return str; } } public class GenericDemo02{ public static void main(String[] args){ Util<Number> util = new Util<Number>(); util.setStr(20); fun(util); } public static void fun(Util<?> tmp){ System.out.println(tmp.getStr()); } }
通配符一共有以下三种方式:
?: 可以接收任意的泛型类型
? extends 类: 指定上限
? super 类: 指定下限