第6章 Java最重要的部分——抽象类与接口
◎ 本章教学微视频:19个 107分钟
学习指引
面向对象编程的过程是一个逐步抽象的过程。接口是比抽象类更高层的抽象,它是对行为的抽象;而抽象类是对一种事物的抽象,即对类的抽象。本章介绍Java的抽象类与接口的相关知识,主要内容包括抽象类和抽象方法、接口的基本知识、接口的多态等。
重点导读
- 掌握抽象类和抽象方法的应用。
- 掌握Java接口的基本知识。
- 掌握Java接口的高级应用。
- 掌握抽象类和接口的应用实例。
- 掌握Java集合框架的使用方法。
6.1 抽象类和抽象方法
在面向对象的概念中,所有的对象都是通过类来描绘的;但是反过来,并不是所有的类都是用来描绘对象的,若一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象方法指一些只有方法声明,而没有具体方法体的方法。抽象方法一般存在于抽象类或接口中。
6.1.1 认识抽象类
假设要编写一个计算圆、三角形和矩形的面积与周长的程序。若按前面所学的方式编程,就必须定义4个类:圆类、三角形类、矩形类和使用前3个类的公共类,它们之间没有继承关系。程序写好后虽然能执行,但从程序的整体结构上看,前3个类之间的许多共同属性和操作在程序中没有很好地被利用,需要重复编写代码,降低了程序的开发效率,且使出现错误的机会增加。
仔细分析上面例子中的前3个类,可以看到这3个类都要计算面积与周长,虽然公式不同,但目标相同。因此,可以为这3个类抽象出一个父类,在父类里定义圆、三角形和矩形3个类共同的成员属性及成员方法。把计算面积与周长的成员方法名放在父类中说明,再将具体的计算公式在子类中实现。
这样,通过父类就大概知道子类所要完成的任务,而且,这些方法还可以应用于求解梯形、平行四边形等其他图形的面积与周长。这种结构就是抽象类的概念。
Java程序用抽象类(abstract class)来实现自然界的抽象概念。抽象类的作用在于将许多有关的类组织在一起,提供一个公共的类,即抽象类。而那些被它组织在一起的具体的类将作为它的子类由它派生出来。抽象类刻画了公有行为的特征,并通过继承机制传递给它的派生类。
抽象类是它的所有子类的公共属性的集合,是包含一个或多个抽象方法的类。使用抽象类的一大优点就是可以充分利用这些公共属性来提高开发和维护程序的效率。
6.1.2 定义抽象类
与普通类相比,抽象类要使用abstract关键字声明。普通类是一个完善的功能类,可以直接产生实例化对象,并且在普通类中可以包含构造方法、普通方法、static方法、常量和变量等内容。而抽象类是在普通类的结构里面增加抽象方法的内容。
【例6-1】(实例文件:ch06\Chap6.1.txt)定义抽象类应用实例。
public abstract class Animal {//定义一个抽象类 //抽象方法没有方法体,用abstract修饰 public abstract void shout(); }
本例中,定义了一个抽象类Animal,有一个抽象方法shout(),注意shout()方法没有方法体,直接以分号结束。抽象类的使用原则如下:
- 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),默认为public。
- 抽象类不能直接实例化,需要依靠子类采用向上转型的方式处理。
- 抽象类必须有子类,使用extends继承,一个子类只能继承一个抽象类。
- 子类如果不是抽象类,则必须重写抽象类中的全部抽象方法(如果子类没有实现父类的抽象方法,则必须将子类也定义为抽象类)。
- 抽象类不能使用final关键字声明,因为抽象类必须有子类,而final定义的类不能有子类。
【例6-2】(实例文件:ch06\Chap6.2.txt)子类继承抽象类应用实例(每个类均为单独的文件)。
本例中,定义了一个子类Dog继承抽象类Animal,并实现了抽象方法shout(),定义了shout()显示狗的叫声。
【例6-3】(实例文件:ch06\Chap6.3.txt)抽象类通过子类向上转型实例化(每个类均为单独的文件)。
程序运行结果如图6-1所示。
图6-1 程序运行结果
抽象类是不能直接实例化的,因此Animal a1 = new Animal();在编译时会报错,那么如何实例化抽象类呢?答案是需要依靠子类采用向上转型的方式来实例化。本例中通过Animal的子类Dog向上转型来实例化:Animal a1 = new Dog();,a1拥有了Dog类重写的shout()方法。总结如下:
- 抽象类继承子类时必须重写方法,而普通类可以有选择地决定是否需要重写方法。
- 抽象类实际上比普通类多了一些抽象方法,其他组成部分和普通类完全一样。
- 普通类对象可以直接实例化,但抽象类的对象必须经过向上转型之后才可以得到。
虽然一个类的子类可以继承任意的一个普通类,可是从开发的实际要求来讲,普通类尽量不要继承另外一个普通类,而应该继承抽象类。
【例6-4】(实例文件:ch06\Chap6.4.txt)抽象类实例(每个类均为单独的文件)。
程序运行结果如图6-2所示。
图6-2 抽象类的应用实例
抽象类在应用的过程中,需要注意以下几点:
- 抽象类不能被实例化,如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类才可以创建对象。
- 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
- 抽象类中的抽象方法只是声明,不包含方法体,也就是不给出方法的具体实现(即方法的具体功能)。
- 构造方法和类方法(用static修饰的方法)不能声明为抽象方法。
- 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。
6.1.3 典型应用实例
抽象类的一个典型应用就是模板设计模式。假设现在有3类不同的对象:机器人、人、猫,这3类对象有不同的行为,分别如下:
- 机器人:充电,工作,关机。
- 人:吃饭,工作,睡觉。
- 猫:进食,逮老鼠,睡觉。
下面编写一个程序,实现3种不同事物的不同行为。
【例6-5】(实例文件:ch06\Chap6.5.txt)抽象类应用:模板设计模式(每个类均为单独的文件)。
程序运行结果如图6-3所示。
图6-3 模板设计模式实例
6.1.4 抽象方法
Java语言中的抽象方法用关键字abstract修饰,这种方法只声明返回的数据类型、方法名称和所需的参数,没有方法体,即抽象方法只需要声明而不需要实现。
1.声明抽象方法
如果一个类包含抽象方法,那么该类必须是抽象类。任何子类必须重写父类的抽象方法,否则子类自身必须声明为抽象类。声明一个抽象类的语法格式如下:
abstract 返回类型 方法名([参数表]);
注意:抽象方法没有定义方法体,方法名后面直接跟一个分号,而不是花括号。
2.抽象方法的实现
继承抽象类的子类必须重写父类的抽象方法,否则,该子类也必须声明为抽象类。最终,必须由子类实现父类的抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。下面通过一个例子介绍子类如何重写父类的抽象方法。
【例6-6】(实例文件:ch06\Chap6.6.txt)定义子类Apple继承抽象类Fruit,重写父类中的方法。
public class Apple extends Fruit{ public Apple(){ color = "红色"; } public void color(){ System.out.println("苹果是:" + color); } public static void main(String[] args) { Apple a = new Apple(); a.color(); } }
程序运行结果如图6-4所示。在本例中,定义继承抽象类Fruit的子类Apple,它实现了父类的抽象方法color(),并重写了自己的构造方法。在程序的main()方法中创建子类对象a,a调用子类Apple实现的抽象方法color()。
图6-4 抽象方法的实现
6.2 接口概述
接口(interface)是Java所提供的另一种重要的技术,接口是一种特殊的类,它的结构和抽象类非常相似,可以认为是抽象类的一种变体。
6.2.1 接口声明
接口是比抽象类更高的抽象,它是一个完全抽象的类,即抽象方法的集合。接口使用关键字interface来声明,语法格式如下:
[public] interface 接口名称 [extends 其他的类名]{ [public][static][final] 数据类型 成员名称=常量值; [public][static][abstract] 返回值 抽象方法名(参数列表); }
接口中的方法是不能在接口中实现的,只能由实现接口的类来实现。一个类可以通过关键字implements来实现。如果实现类没有实现接口中的所有抽象方法,那么该类必须声明为抽象类。下面是接口声明的一个简单例子。
【例6-7】(实例文件:ch06\Chap6.7.txt)接口声明。
public interface Shape { public double area(); //计算面积 public double perimeter(); //计算周长 }
使用关键字interface声明了一个接口Shape,并在接口内定义了两个抽象方法area()和perimeter()。接口有以下特性:
- 接口中也有变量,但是接口会隐式地指定为public static final变量,并且只能是public,用private修饰会报编译错误。
- 接口中的抽象方法具有public和abstract修饰符,也只能是这两个修饰符,其他修饰符都会报错。
- 接口是通过类来实现的。
- 一个类可以实现多个接口,多个接口之间使用逗号(,)隔开。
- 接口可以被继承,被继承的接口必须是另一个接口。
6.2.2 实现接口
当类实现接口的时候,类要实现接口中所有的方法,否则类必须声明为抽象的类。类使用implements关键字实现接口。在类声明中,implements关键字放在class声明后面。实现一个接口的语法如下:
class 类名称 implements 接口名称[,其他接口]{ … }
下面给出一个实现接口的实例。
【例6-8】(实例文件:ch06\Chap6.8.txt)实现接口(每个类均为单独的文件)。
在本例中,定义了两个类Circle和Rectangle,它们分别实现了接口Shape,并实现了接口定义的两个抽象方法,用来计算面积和周长。在实现接口的时候,要注意以下规则:
- 一个类可以同时实现多个接口。
- 一个类只能继承一个类,但是能实现多个接口。
- 一个接口能继承另一个接口,这和类之间的继承比较相似。
- 重写接口中声明的方法时,需要注意以下规则。
- ◆ 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
- ◆ 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
- ◆ 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。
6.2.3 接口默认方法
Java提供了接口默认方法。即允许接口中可以有实现方法,使用default关键字在接口修饰一个非抽象的方法,这个特征又叫扩展方法。
【例6-9】(实例文件:ch06\Chap6.9.txt)接口默认方法。
public interface InterfaceNew { public double method(int a); public default void test() { System.out.println("Java 8接口新特性"); } }
在本例中,定义了接口InterfaceNew,除了声明抽象方法method()外,还定义了使用default关键字修饰的实现方法test(),实现了InterfaceNew接口的子类只需实现一个calculate()方法即可,test()方法在子类中可以直接使用。
6.2.4 接口与抽象类
接口的结构和抽象类非常相似,也具有数据成员与抽象方法,但它又与抽象类不同。下面详细介绍接口与抽象类的异同。
1.接口与抽象类的相同点
接口与抽象类存在一些相同的特性,具体如下:
- 都可以被继承。
- 都不能被直接实例化。
- 都可以包含抽象方法。
- 派生类必须实现未实现的方法。
2.接口与抽象类的不同点
接口与抽象类还有一些不同之处,具体如下:
- 接口支持多继承,抽象类不能实现多继承。
- 一个类只能继承一个抽象类,但可以实现多个接口。
- 接口中的成员变量只能是public static final类型的,抽象类中的成员变量可以是各种类型的。
- 接口只能定义抽象方法;抽象类既可以定义抽象方法,也可以定义实现的方法。
- 接口中不能含有静态代码块以及静态方法(用static修饰的方法),抽象类可以有静态代码块和静态方法。
6.3 接口的高级应用
接口在Java中是最重要的概念之一,它可以被理解为一种特殊的类,是由全局常量和公共的抽象方法所组成的。需要注意的是,在接口中的抽象方法必须定义为public访问权限,这是不可更改的。
6.3.1 接口的多态
Java中没有多继承,一个类只能有一个父类。而继承的表现就是多态,一个父类可以有多个子类,而在子类里可以重写父类的方法,这样每个子类里重写的代码不一样,自然表现形式就不一样。
用父类的变量去引用不同的子类,在调用这个相同的方法的时候得到的结果和表现形式就不一样了,这就是多态,调用相同的方法会有不同的结果。下面给出一个实例。
【例6-10】(实例文件:ch06\Chap6.10.txt)接口的多态,基于实现接口的实例。
运行结果如图6-5所示。
在本例中,Shape是一个接口,没有办法实例化对象,但可以用Circle类和Rectangle类来实例化对象,也就实现了接口的多态。实例化产生的对象s1和s2拥有同名的方法,但各自实现的功能却不一样。根据实现接口的类中重写的方法,实现了用同一个方法计算不同图形的面积和周长的功能。
图6-5 接口的多态应用实例
6.3.2 适配接口
在实现一个接口时,必须实现该接口的所有方法,这样有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要使用其中的一些方法。为了解决这个问题,引入了接口的适配器模式,借助于一个抽象类来实现该接口所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系。写一个类,继承该抽象类,再重写需要的方法就行。
【例6-11】(实例文件:ch06\Chap6.11.txt)适配接口(每个类均为单独的文件)。
程序运行结果如图6-6所示。
图6-6 适配接口应用实例
在本例中,首先定义了一个接口InterfaceAdapter,并定义了两个抽象方法email()和sms()。然后定义了一个抽象类Wrapper,并实现了两个抽象方法,但方法体为空。定义了一个类S1,重写了email()方法。这样写的好处是,定义类时不需要直接实现接口InterfaceAdapter并实现定义的两个方法,而只需要实现并重写email()方法即可。
6.3.3 嵌套接口
在Java语言中,接口可以嵌套在类或其他接口中。由于Java中在interface内是不可以嵌套class的,所以接口的嵌套共有两种方式:class内嵌套interface、interface内嵌套interface。
1.class内嵌套interface
这时接口可以是public、private和package。重点在private上,被定义为私有的接口只能在接口所在的类中实现。可以被实现为public的类也可以被实现为private。当被实现为public时,只能在自身所在的类内部使用。只能够实现接口中的方法,在外部不能像正常类那样上传为接口类型。
2.interface内嵌套interface
由于接口的元素必须是public的,所以被嵌套的接口自动就是public的,而不能定义成private的。在实现这种嵌套时,不必实现被嵌套的接口。
【例6-12】(实例文件:ch06\Chap6.12.txt)嵌套接口举例(每个类均为单独的文件)。
本例中,语句A.D ad = a.getD();和a.getD().f();产生编译错误,这是因为D是A的私有接口,不能在外部被访问。语句A.DImp2 di2 = a.getD();的错误是因为getD()方法的返回值类型为D,不能自动向下转型为DImp2类型。
6.3.4 接口回调
接口回调是指可以把使用某一接口的类创建的对象的引用赋给该接口声明的接口变量,那么该接口变量就可以调用被类实现的接口的方法。实际上,当接口变量调用被类实现的接口中的方法时,就是通知相应的对象调用接口的方法,这一过程称为对象功能的接口回调。下面看一个例子。
【例6-13】(实例文件:ch06\Chap6.13.txt)接口回调,基于实现接口的例子(每个类均为单独的文件)。
程序运行结果如图6-7所示。
图6-7 接口回调应用实例
本例中定义了一个类Show,其中定义了一个方法print(),将Shape类型的变量作为参数。在测试时,实例化Show,并调用print()方法,将new Circle()和new Rectangle()作为实际参数,因此会调用不同的方法,结果显示不同图形的面积和周长。
6.4 抽象类和接口的实例
在介绍了抽象类和接口的声明和使用以及抽象方法的使用后,本节举例介绍抽象类与接口的使用。
6.4.1 抽象类的应用实例
抽象类本身是不能直接实例化的,因为其本身包含抽象方法。不过,抽象类可以通过对象的多态性来实例化,即抽象类通过子类进行实例化操作。那么可以将抽象类作为某个类型的模板,而具体的子类通过继承该抽象类,重写其中的方法来实现。下面看一个例子。
【例6-14】(实例文件:ch06\Chap6.14.txt)抽象类的实际应用(每个类均为单独的文件)。
程序运行结果如图6-8所示。
图6-8 抽象类的应用
首先定义一个抽象类Person,定义一个抽象方法call(),然后定义两个类Teacher和Student,分别继承抽象类Person,并分别实现了方法call();定义类Lesson,定义方法lessonBegin(),使用Person类型的变量作为参数,在测试时实例化Lesson,并使用new Teacher()和new Student()作为实际参数。最后老师会说“同学们好!”,学生说“老师好!”。
6.4.2 接口的应用实例
接口的作用就是将方法名称开放给用户。接口与抽象类一样,需要通过子类进行实例化操作。接口的实际应用更类似于定义标准。下面看一个例子。
【例6-15】(实例文件:ch06\Chap6.15.txt)接口的实际应用,以USB接口为例(每个类均为单独的文件)。
程序运行结果如图6-9所示。
图6-9 接口的实际应用
首先定义接口USB,定义两个抽象方法start()和stop(),然后定义两个类Mouse和Keyboard,分别实现了接口USB,并实现了两个方法start()和stop()。定义类MainBoard,定义方法plugIn(),使用USB类型的变量作为参数,在测试时实例化MainBorad,并使用new Mouse()和new Keyborad()作为实际参数,
另外,接口在实际编程中用途非常广泛,在设计模式中也有很多应用,下面讲解设计模式中的工厂模式。
【例6-16】(实例文件:ch06\Chap6.16.txt)接口应用:工厂模式。
interface Fruit { public void eat() ; } class Apple implements Fruit { public void eat() { System.out.println("吃苹果。") ; } } class Orange implements Fruit { public void eat() { System.out.println("吃橘子。") ; } } class Factory1 { //此类不需要维护属性的状态 public static Fruit getInstance(String className) { if ("apple".equals(className)) { return new Apple() ; } if ("orange".equals(className)) { return new Orange() ; } return null ; } } public class Factory { public static void main(String args[]) { Fruit f = Factory1.getInstance(args[0]) ; //初始化参数 f.eat() ; } }
当args[0]为apple时,程序运行结果如图6-10所示。
图6-10 当args[0]为apple时程序运行结果
当args[0]为orange时,程序运行结果如图6-11所示。
图6-11 当args[0]为orange时程序运行结果
输入参数的方法如下:选择菜单栏中的Run→Run Configurations命令,如图6-12所示。
图6-12 选择Run Configurations命令
打开如图6-13所示的窗口,在Program arguments文本框中输入apple,这样就表示args[0]为apple,调试结果控制台会显示“吃苹果”。如果将apple改为orange,则会显示“吃橘子”。
图6-13 Run Configurations对话框
本例中,首先定义了一个水果接口Fruit,其中定义了一个抽象方法eat(),然后定义了Apple类和Orange类分别实现了接口Fruit,并实现了抽象方法eat(),之后定义了类Factory1,最后定义类Factory,并根据参数内容实例化不同的子类。
根据args[0]的内容实例化不同的子类。如果为apple,实例化Apple类;如果为orange,则实例化Orange类。因此输出的内容也不同。
6.5 Java的集合框架
在Java语言中有一个由设计优良的接口和类组成的Java集合框架,以方便程序员操作成批的数据或对象元素。本节详细介绍集合框架的使用。
6.5.1 接口和实现类
Java语言中的集合框架就是一个类库的集合,包含了实现集合框架的接口。集合框架就像一个容器,用来存储Java类的对象。Java所提供的集合API都在java.util包中。集合框架结构如图6-14所示。
图6-14 集合框架结构
1.Java集合框架中的接口
在Java集合框架中提供了以下接口:
- Collection接口。该接口定义了存取一组对象的方法,是最基本的接口。
- Set接口。该接口继承Collection接口,它包含的数据没有顺序且不可以重复。
- List接口。该接口继承Collection接口,它包含的数据有顺序且可以重复。
- Map接口。该接口是一个单独的接口,不继承Collection接口。它是一种把键对象和值对象进行关联的容器,不可以包含重复的键。
2.Java集合框架中的实现类
在Java集合框架中提供了实现接口的以下类:
- HashSet。实现了Set接口,为无序集合,能够快速定位一个元素。需要注意的是,存入HashSet中的对象必须实现HashCode()方法。
- TreeSet。不仅实现了Set接口,还实现了Sorted接口,可对集合进行自然排序。
- ArrayList。实现了List接口,为有序集合,它的大小可变并且可以像链表一样被访问。它是以数组的方式实现的List接口,允许快速随机存取。
- LinkedList。实现了List接口,为有序集合,通过一个链表的形式实现List接口,提供最佳顺序存取,适合插入和移除元素。由这个类定义的链表也可以像栈或队列一样被使用。
- HashMap。实现一个“键-值”映射的哈希表,通过键获取值对象,没有顺序,通过get(key)方法来获取value的值,允许存储空对象,而且允许键是空的。不过,键只能有一个。
6.5.2 Collection接口
Collection接口是Set接口和List接口的父接口,是最基本的接口。Collection接口定义了对集合进行基本操作的一些通用方法。由于Set接口和List接口继承自Collection接口,所以可以调用这些方法。Collection接口提供的主要方法如表6-1所示。
表6-1 Collection接口中的方法
在所有实现Collection接口的容器类中都有一个iterator()方法,此方法返回一个实现了Iterator接口的对象。Iterator对象称作迭代器,方便实现对容器内元素的遍历操作。
由于Collection是一个接口,不能直接实例化。下面的例子是通过ArrayList实现类来调用Collection接口的方法。
【例6-17】(实例文件:ch06\Chap6.17.txt)Collection接口方法的使用。
程序运行结果如图6-15所示。
图6-15 Collection接口方法的使用
在本例中,定义了接口List的实现类ArrayList的对象c和array,通过add()方法为两个集合添加元素。
- 集合array调用isEmpty()方法,在集合array不为空的情况下,集合c调用addAll()方法,将集合array中的元素全部添加到集合c中。
- 集合c调用iterator()方法返回迭代器对象Iterator,通过while循环输出集合c中的元素。iterator.hasNext()方法判断迭代器中是否有下一个元素,如有则执行iterator.next()方法输出元素。
- 集合c调用contains()方法判断集合c是否包含指定的元素;调用removeAll()方法移出array集合的元素;调用toArray()方法将集合元素存放到数组str中,并通过for循环输出数组元素。
注意:任何对象加入集合类后都自动转变为Object类型,所以在取出的时候需要进行强制类型转换。
6.5.3 List接口
List接口是Collection的子接口,实现List接口的容器类中的元素是有顺序的,并且元素可以重复。List容器中的元素对应一个整数型的序号,记录其在List容器中的位置,可以根据序号存取容器中的元素。List接口除了继承Collection接口的方法外,又提供了一些方法,如表6-2所示。
表6-2 List接口中的方法
在Java API中提供的实现List接口的容器类有ArrayList、LinkedList等,它们是有序的容器类。使用哪个实现类要根据具体的场合来定。
1.ArrayList类
ArrayList类实现一个可变大小的数组,可以像链表一样被访问。它以数组的方式实现,允许快速随机存取。它允许包含所有元素,包括null元素。每个ArrayList类实例都有一个容量(capacity),即存储元素的数组大小,这个容量可以随着不断添加新元素而自动增加。
ArrayList类常用的构造方法有3种重载形式,具体如下:
(1)构造一个初始容量为10的空列表。
public ArrayList()
(2)构造一个指定初始容量的空列表。
public ArrayList(int initialCapacity)
(3)构造一个包含指定集合元素的列表,这些元素是按照该集合的迭代器返回它们的顺序排列。
public ArrayList(Collection c)
【例6-18】(实例文件:ch06\Chap6.18.txt)ArrayList类的使用。
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class ArrrayListTest { public static void main(String[] args) { ArrayList list = new ArrayList(); //创建初始容量为10的空列表 list.add("cat"); list.add("dog"); list.add("pig"); list.add("sheep"); list.add("pig"); System.out.println("---输出集合中的元素---"); Iterator iterator = list.iterator(); while(iterator.hasNext()){ System.out.print(iterator.next()+" "); } System.out.println(); //替换指定索引处的元素 System.out.println("返回替换集合中索引是1的元素:" + list.set(1, "mouse")); iterator = list.iterator(); System.out.println("---元素替换后集合中的元素---"); while(iterator.hasNext()){ System.out.print(iterator.next()+" "); } System.out.println(); //获取指定索引处的集合元素 System.out.println( "获取集合中索引是2的元素:"+ list.get(2)); System.out.println("集合中第一次出现pig的索引:" + list.indexOf("pig")); System.out.println("集合中最后一次出现dog的索引:" + list.lastIndexOf("dog")); List l = list.subList(1, 4); iterator = l.iterator(); System.out.println("---新集合中的元素---"); while(iterator.hasNext()){ System.out.print(iterator.next()+" "); } } }
程序运行结果如图6-16所示。
图6-16 ArrayList类方法的使用
在本例中,定义了一个ArrayList类的对象list,list调用add()方法添加集合元素,通过它的iterator()方法获取迭代器对象iterator,通过iterator对象和while循环输出集合中的元素,可以看出集合中的元素就是按照add()方法的添加顺序排列的。
list集合调用set(1)方法替换集合中指定索引1处的元素dog为mouse;调用get(2)方法获取指定索引2处的元素,返回pig;调用indexOf("pig")方法获取指定元素pig第一次出现的索引,即2;调用lastIndexOf("dog")方法获取指定元素dog最后一次出现的索引,由于dog被mouse替换,不存在dog,所以返回-1;调用subList(1, 4)方法返回集合中从指定开始索引1到结束索引4间的一个新集合,不包含结束索引4处的元素。
注意:调用subList()方法返回的新集合中不包含结束索引处的元素。
2.LinkedList类
LinkedList实现了List接口,允许null元素。LinkedList类实现一个链表,可以对集合的首部和尾部进行插入和删除操作,这些操作可以使LinkedList类被用作堆栈(stack)、队列(queue)或双向队列(deque)。相对于ArrayList,LinkedList在插入或删除元素时提供了更好的性能,但是随机访问元素的速度则相对较慢。LinkedList类除了继承List接口的方法,又提供了一些方法,如表6-3所示。
表6-3 LinkedList类的方法
【例6-19】(实例文件:ch06\Chap6.19.txt)LinkedList类提供的方法的使用。
import java.util.Iterator; import java.util.LinkedList; public class LinkedListTest { public static void main(String[] args) { LinkedList list = new LinkedList(); //创建初始容量为10的空列表 list.add("cat"); list.add("dog"); list.add("pig"); list.add("sheep"); list.addLast("mouse"); list.addFirst("duck"); System.out.println("---输出集合中的元素---"); Iterator iterator = list.iterator(); while(iterator.hasNext()){ System.out.print(iterator.next()+" "); } System.out.println(); System.out.println("获取集合的第一个元素:" + list.getFirst()); System.out.println("获取集合的最后一个元素:" + list.getLast()); System.out.println("删除集合的第一个元素" + list.removeFirst()); System.out.println("删除集合的最后一个元素" + list.removeLast()); System.out.println("---删除元素后集合中的元素---"); iterator = list.iterator(); while(iterator.hasNext()){ System.out.print(iterator.next()+" "); } } }
程序运行结果如图6-17所示。
图6-17 LinkedList方法的使用
在本例中,定义一个链表集合list,通过实现类LinkedList自定义的addFirst()方法和addLast()方法,在链表的首部和尾部添加元素duck和mouse,可以看出添加元素的位置与这两个方法的位置无关;但是在使用add()方法时,添加元素顺序和add()方法顺序有关。list调用getFirst()方法和getLast()方法获取集合中第一个和最后一个元素,即duck和mouse;调用removeFirst()方法和removeLast()方法删除集合中第一个和最后一个元素。
注意:LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。
6.5.4 Set接口
Set接口是Collection的子接口,Set接口没有提供新增的方法。实现Set接口的容器类中的元素是没有顺序的,并且元素不可以重复。在Java API中提供的实现Set接口的容器类有HashSet、TreeSet等,它们是无序的容器类。
1.HashSet类
HashSet类实现了Set接口,不允许出现重复元素,不保证集合中元素的顺序,允许包含值为null的元素,但最多只能有一个。HashSet添加一个元素时,会调用元素的hashCode()方法,获得其哈希码,根据这个哈希码计算该元素在集合中的存储位置。HashSet使用哈希算法存储集合中的元素,可以提高集合元素的存储速度。
HashSet类的常用构造方法有3种重载形式,具体如下:
(1)构造一个新的空Set集合。
public HashSet()
(2)构造一个包含指定集合中的元素的新Set集合。
public HashSet(Collection c)
(3)构造一个新的空Set集合,指定初始容量。
public HashSet(int initialCapacity)
【例6-20】(实例文件:ch06\Chap6.20.txt)HashSet类的使用。
import java.util.HashSet; import java.util.Iterator; public class HashSetTest { public static void main(String[] args) { HashSet hash = new HashSet(); hash.add("56"); hash.add("32"); hash.add("50"); hash.add("48"); hash.add("48"); hash.add("23"); System.out.println("集合元素个数:" + hash.size()); Iterator iter = hash.iterator(); while(iter.hasNext()){ System.out.print(iter.next() + " "); } } }
程序运行结果如图6-18所示。
图6-18 HashSet的使用
在本例中,定义了HashSet对象hash,通过调用它的add()方法添加集合元素,可以看到添加的重复元素48被覆盖,Set集合中不允许存在重复元素。通过hash对象调用iterator()方法获得迭代器,输出集合中的元素,可以看到元素是无序的。
2.TreeSet类
TreeSet类不仅继承了Set接口,还继承了SortedSet接口,它不允许出现重复元素。由于SortedSet接口可以对集合中的元素进行自然排序(即升序排序),因此TreeSet类会对实现了Comparable接口的类的对象自动排序。TreeSet类提供的方法如表6-4所示。
表6-4 TreeSet类提供的方法
【例6-21】(实例文件:ch06\Chap6.21.txt)TreeSet类方法的使用。
程序运行结果如图6-19所示。
图6-19 TreeSet方法的使用
在本例中,定义了TreeSet对象tree,对象tree调用add()方法添加集合元素,这里添加的集合元素是String类的,由于String类实现了Comparable接口,所以TreeSet类对添加的元素进行了升序排序。
6.5.5 Map接口
Map接口是用来存储“键-值”对的集合,存储的“键-值”对是通过键来标识的,所以键不可以重复。Map接口的实现类有HashMap类和TreeMap类。
HashMap是基于哈希表的Map接口的实现类,以“键-值”对映射的形式存在。它在HashMap中总是被当作一个整体,系统会根据哈希算法来计算“键-值”对的存储位置,具有很快的访问速度,最多允许一条记录的键为null,不支持线程同步。可以通过键快速地存取值。TreeMap类继承了AbstractMap,可以对键对象进行排序。Map接口提供的方法如表6-5所示。
表6-5 Map接口提供的方法
【例6-22】(实例文件:ch06\Chap6.22.txt)HashMap类方法的使用。
import java.util.Iterator; import java.util.Set; import java.util.HashMap; public class HashMapTest { public static void main(String[] args) { HashMap map = new HashMap(); map.put("101", "一代天骄"); //添加“键-值”对 map.put("102", "成吉思汗"); //添加“键-值”对 map.put("103", "只识弯弓射大雕"); //添加“键-值”对 map.put("104", "俱往矣"); //添加“键-值”对 map.put("105", "数风流人物"); //添加“键-值”对 map.put("105", "还看今朝"); //添加“键-值”对 System.out.println("指定键102获取值:" + map.get("102")); Set s = map.keySet(); //获得HashMap键的集合 Iterator iterator = s.iterator(); //获得HashMap中值的集合并输出 String key = ""; while(iterator.hasNext()){ key = (String)iterator.next(); //获得HashMap键的集合,强制转换为String型 System.out.println(key + ":" + map.get(key)); } } }
程序运行结果如图6-20所示。
图6-20 HashMap类的使用
在本例中,定义了HashMap类的对象map,通过调用put()方法添加集合元素。map对象调用get("102")方法,获得指定键的值“成吉思汗”。map调用iterator()方法获得Iterator对象,通过iterator.next()方法获得HashMap类中的键,并赋值给String型的key;map调用get(key)方法获得HashMap类中的值,最后打印显示键值对。
6.6 就业面试解析与技巧
6.6.1 面试解析与技巧(一)
面试官:抽象类和接口有什么异同?
应聘者:抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口,都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造方法,而且其中的方法都是抽象方法。
抽象类中的成员可以是private、默认、protected、public的,而接口中的成员全都是public的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
6.6.2 面试解析与技巧(二)
面试官:Comparable接口有什么作用?
应聘者:当需要排序的集合或数组不是单纯的数字类型时,通常可以使用Comparable接口,以简单的方式实现对象排序或自定义排序。这是因为Comparable接口内部有一个要重写的关键的方法,即compareTo(T o),用于比较两个对象的大小,这个方法返回一个整型数值。例如x.compareTo(y),情况如下:如果x和y相等,则方法返回值是0;如果x大于y,则方法返回值是大于0的值;如果x小于y,则方法返回小于0的值。因此,可以对实现了Comparable接口的类的对象进行排序。