泛型与容器类
泛型
泛型的概念
在定义类、接口或方法时通过为其增加“类型参数”可以实现泛型,将所操作的数据类型泛化成一个参数(类型参数)。当这种类型参数用在类、接口以及方法的声明中时,则分别称为泛型类、泛型接口和泛型方法。按照通常的惯例,用T或E这样的大写字母来表示类型参数。
泛型类的定义:[修饰符] class 类名<T>
泛型接口的定义:[public] interface 接口名<T>
泛型方法的定义:[public] [static] <T> 返回值类型 方法名
定义了泛型之后,就可以在代码中使用类型参数T来表示一种自定义的数据类型。而在应用这些具有泛型特性的类或接口时,需要指明实际的具体类型来替换类型参数T。
泛型的概念实际上是基于“类型也可以像变量一样实现参数化”这一简单的设计理念实现的,因此泛型也称为参数多态。
泛型类及应用
定义泛型类后,类中就多了一个自定义的数据类型T,地位与正常的数据类型相同。实例化泛型类时,在<>中传递正常的数据类型就可以替换泛型类中全部的T,但注意:<>中传入的实际类型必须是引用类型,即必须是类类型(如String),不能用如int、double或char等这样的基本类型来替换类型T。
public class Test <T>{
private T obj;//这是一个T类型的变量obj
//添加get、set方法
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
//写个主方法测试一下
public static void main(String[] args) {
Test<String> name=new Test<String>();//将T替换为String
Test<Integer> age=new Test<Integer>();//试图传入int,但编译报错,检查应替换为Integer
name.setObj("张三");
age.setObj(30);
System.out.println(name.getObj()+"是"+age.getObj()+"岁");
}
}
注意:在JDK5中新增了自动包装和自动解包功能,实现了int、double、char等基本类型与Integer、Double、Character等类类型的自动转换。
age.setObj(30);//自动包装:int-->Integer
int newAge= age.getObj();//自动解包:Integer-->int
补充:当一个泛型有多个类型参数时,每个类型参数在该泛型中都应该是唯一的。例如,不能定义形如Map<K,K>形式的泛型,而应该定义成Map<K,V>形式将两个类型参数区分开来。
泛型方法
一个方法是否是泛型方法与它所在的类是否是泛型类没有关系。要定义泛型方法,只需将泛型的类型参数<T>置于方法返回值类型前即可。Java中任何方法(包括静态方法和构造方法)都可以声明为泛型方法。泛型方法除了定义不同,调用时与普通方法一样。
public class Test {
//定义泛型方法,打印不同类型的数组
public static <T>void display(T[] list){
for (int i = 0; i < list.length; i++) {
System.out.print(list[i]+" ");
}
System.out.println();
}
//写个主方法测试一下
public static void main(String[] args) {
Integer[] numList={1,2,3,4,5};
String[] strList={"红","橙","黄","绿","青","蓝","紫"};
Test.display(numList);//直接调用类的静态方法
Test.display(strList);
}
}
输出结果:
1 2 3 4 5
红 橙 黄 绿 青 蓝 紫
说明:为了强调是泛型方法,也可以将实际类型放在尖括号内作为方法名的前缀,例如Test.<String>display(strList)。一般来说编写Java泛型方法时,返回值类型和至少一个参数类型应该是泛型,而且类型应该是一致的,如果只有返回值类型或参数类型之一使用了泛型,这个泛型方法的使用就大大地受限制,基本限制到跟不用泛型一样的程度。所以推荐使用返回值类型和参数类型一致的泛型方法。Java泛型方法广泛使用在方法返回值和参数均是容器类对象时。
注意:若泛型方法的多个形式参数使用了相同的类型参数T,并且对应的多个实参具有不同的类型,则编译器会将该类型参数T指定为这多个实参的类型所具有的“最近”共同父类直至Object
说明:static方法,是无法访问泛型类的类型参数的,所以如果static方法需要使用泛型能力,必须使其成为泛型方法。
补充:当使用泛型类时,必须在创建泛型对象的时候指定类型参数T的实际值,而调用泛型方法时,通常不必指明参数的类型,因为编译器有个功能叫类型参数推断,类型推断只对赋值操作有效,此时编译器会根据赋值,找出具体的类型。类型推断其他时候并不起作用。
public class Test <T>{
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
//定义泛型方法
public <U>void display(U u){
System.out.println("泛型类的类型参数T:"+t.getClass().getName());
System.out.println("泛型方法的类型参数U:"+u.getClass().getName());
}
//写个主方法测试一下
public static void main(String[] args) {
Test<Integer> test=new Test<Integer>();
test.setT(5);
System.out.println("第一次输出:");
test.display("我是文本");
System.out.println("第二次输出:");
test.display(8.0);
}
}
输出结果:
第一次输出:
泛型类的类型参数T:java.lang.Integer
泛型方法的类型参数U:java.lang.String
第二次输出:
泛型类的类型参数T:java.lang.Integer
泛型方法的类型参数U:java.lang.Double
由此可见,display()方法分别输出泛型类的类型参数T和泛型方法的类型参数U的类型。
从泛型方法的调用可以看出,在调用泛型方法display()时,并没有显式地传入实参的类型,而是像普通方法调用一样,用实参的值去调用该方法。这就是Java编译器的类型参数推断,它会根据调用方法时传入的实参的类型,推断出被调用的方法的类型参数的具体类型。
设计泛型方法的目的主要是针对具有容器类型参数的方法的。如果编写的代码并不接受和处理容器类型,就根本不需要使用泛型方法(详见容器类)
泛型方法与泛型类在传递类型实参方面的一个重要区别是:对于泛型方法,不需要把实际的类型传递给泛型方法;但泛型类恰恰相反,即必须要把实际的类型参数传递给泛型类。
限制泛型的可用类型
在定义泛型类时,默认可以使用任何类型来实例化一个泛型类对象,但Java语言也可以在用泛型类创建对象时对数据类型做出限制。其语法如下:
class ClassName<T extends anyClass>
其中,anyClass是指某个类或接口。
该语句表示T是ClassName类的类型参数,且T有一个限制,即T必须是anyClass类或是继承了anyClass类的子类或是实现了anyClass接口的类。
注意:对于实现了某接口的有限制泛型,也是用extends关键字,而不是用implements关键字。
案例如下:
public class Test <T extends Number>{
private T t;
public T getT() {
return t;
}
//定义泛型类的构造方法
public Test(T t) {
this.t = t;
}
//写个主方法测试一下
public static void main(String[] args) {
Test<Integer> test1=new Test<Integer>(5);
System.out.println("给出的参数是:"+test1.getT());
//下面两行语句是非法的,因为实际参数的类型是String,不是Number或Number的子类
// Test<Integer> test2=new Test<Integer>("Hello");
// System.out.println("给出的参数是:"+test2.getT());
}
}
输出结果:
给出的参数是:5
说明:在定义泛型类时若没有使用extends关键字限制泛型的类型参数,那么默认是Object类下的所有子类,即<T>和<T extends Object>是等价的。
补充:虽然Integer是Number的子类,但Test<Integer>不是Test<Number>的子类,这种限定称为“泛型不是协变的”。
泛型的类型通配符和泛型数组的应用
在泛型机制中除了有限制的泛型类之外,还引入了通配符"?"的概念,其主要作用有两个方面:一是用于创建可重新赋值但不可修改其内容的泛型对象;二是用在方法的参数中,限制传入不想要的类型参数。
当需要在一个程序中使用同一个泛型对象名去引用不同的泛型对象时,就需要使用通配符"?“创建泛型类对象,但条件是被创建的这些不同泛型对象的类型实参必须是某个类或是继承该类的子类又或是实现某个接口的类。也就是说,只知道通配符”?"表示是某个类又或是继承该类的子类又或是实现某个接口的类,但具体是什么类型不知道。如下语句是用泛型类创建泛型类对象:
泛型类名<? extends T> o=null;//声明泛型类对象o
其中,“? extends T”表示是T或T的未知子类型或是实现接口T的类。所以在创建泛型对象o时,若给出的类型参数不是类T或T的子类或是实现接口T的类,则编译时报告出错。
public class Test <T>{
//写个主方法测试一下
public static void main(String[] args) {
//n的类型实参只能接收实现了List接口的类类型
Test<? extends List> n=new Test<LinkedList>();
n=new Test<ArrayList>();
//由于HashMap没有实现List接口,故如下赋值报错
// n=new Test<HashMap>();
}
}
通配符"?"除了在创建泛型类对象时限制泛型类的类型之外,还可以将由通配符限制的泛型类对象用在方法的参数中以防止传入不允许接收的类型参数。示例如下:
public class Test <T>{
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
//下面的方法接收的泛型类对象参数中的类型参数只能是String或String类的子类
public static void showT(Test<? extends String> o){
System.out.println("给出的值是:"+o.getT());
}
//写个主方法测试一下
public static void main(String[] args) {
Test<String> n=new Test<String>();
n.setT("张三");
Test.showT(n);
//错误示范,传入Double类型数据时不可调用show()方法
Test<Double> num=new Test<Double>();
num.setT(25.0);
// Test.showT(num);
}
}
在创建泛型类对象时,如果只使用了"?“通配符,则默认是”? extends Object",所以"?"也被称为非受限统配。
对于一个泛型类Test<T>来说,在创建相应的泛型类对象时,参数类型T除了用某个实际类型替换外,还可以用通配符"?",但在赋值时要注意范围约束:
public class Test <T>{
//写个主方法测试一下
public static void main(String[] args) {
Test<String> test1=new Test<String>();
Test<?> test2=new Test<String>();
test2=test1;
//test2对象可以接收Object类下的任何类类型,范围比test1大,故如下赋值报错
// test1=test2;
}
}
除此之外,用通配符"?"创建的对象只能获取和删除其中的信息,但不能对其加入新的信息:
public class Test <T>{
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
//写个主方法测试一下
public static void main(String[] args) {
Test<? extends String> test=null;
test.getObj();//获取对象内的信息
test.setObj(null);//删除对象内的信息
}
}
直接使用通配符<?>创建泛型对象,有两个特点
- 具有通用性,即改泛型类的其他对象可以赋值给用通配符"?“创建的泛型对象,因为”?“等价于”? extends Object",反之不可。
- 用通配符"?"创建的泛型对象只能获取或删除其中的信息,但不可为其添加新的信息。
在泛型通配符"? extends T"中,由于T被认为是类型参数"?“的上限,所以”? extends T"也被称为上限通配;当然也可以对类型参数进行下限限制,此时只需将extends改为super即可,"? extends T"表示是T或T的一个未知父类型,T表示类型参数"?"的下限,所以被称为下限通配。
引入通配符的主要目的是支持泛型中的子类,从而实现多态。如果泛型方法的目的只是为了能够适用于多个不同类型或支持多态,则应选用通配符。泛型方法中类型参数的优势是可以表达多个参数之间或参数与返回值之间的类型依赖关系,如果方法中并不存在类型之间的依赖关系,则可以不使用泛型方法,而选用通配符。一般地,由于通配符更清晰、更简明,因此在程序开发过程中建议尽量采用通配符。
定义泛型类时也可以声明数组:
public class Test <T>{
private T[] array;
public T[] getArray() {
return array;
}
public void setArray(T[] array) {
this.array = array;
}
//写个主方法测试一下
public static void main(String[] args) {
Test<String> a=new Test<String>();
String[] array={"红色","橙色","黄色","绿色","青色","蓝色","紫色"};
a.setArray(array);
for (int i = 0; i < a.getArray().length; i++) {
System.out.print(a.getArray()[i]+" ");
}
}
}
输出结果:
红色 橙色 黄色 绿色 青色 蓝色 紫色
注意:
- 泛型的类型参数T不能超出作用域-
- 不要在静态环境中使用T,因为静态环境被创建时T还没接收实际类型参数
- 异常类不能是泛型的,即泛型类不能继承java.lang.Throwable类
继承泛型类与实现泛型接口
被定义为泛型的类或接口可以被继承与实现。例如:
public class Father <T1>{ }
class Son <T1,T2,T3> extends Father<T1>{ }
在Son类继承Father类时如果要保留父类的类型参数,需要在继承时指明,如果没有指明,直接使用extends Father语句进行继承声明,则Son类中的T1,T2,T3都会自动变成Object。一般情况下都将父类的类型参数保留。
在定义泛型接口时,泛型接口也可被实现。例如:
interface Father <T1>{ }
class Son <T1,T2> implements Father<T1>{ }
容器类
容器类是Java以类库的形式供用户开发程序时可直接使用的各种数据结构。所谓数据结构就是以某种方式将数据组织在一起,并存储在计算机中。数据结构不仅可以存储数据,还支持访问和处理数据的操作。在面向对象思想里,一种数据结构被认为是一个容器。数组是一种简单的数据结构,除数组外Java还以类库的形式提供了许多其他数据结构。这些数据结构通常被称为容器类或称集合类。
Java容器框架
Java容器框架中有两个名称分别为Collection和Set的接口,这里将Collection译为容器,将Set译为集合。
Java容器框架提供了一些现成的数据结构可供使用,这些数据结构是可以存储对象的集合,集合中存储的对象也被称为元素。
从JDK5开始,容器框架全部采用泛型实现,且都存放在java.util包中。容器框架中的接口以及实现这些接口的类的继承关系如下所示(详见书p233)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YXvfF69S-1613285027617)(D:\课件\科创\截图\容器框架.jpg)]
Java容器框架结构由两棵接口树构成(详见数据结构—树),第一棵树根节点为Collection接口,它定义了所有容器的基本操作,如添加、删除、遍历等。它的子接口Set、List等则提供了更加特殊的功能,其中Set的对象用于存储一组不重复元素的集合,而List的对象用于存储一个由元素构成的线性表。第二棵树根节点为Map接口,它保持了“键”到“值”的映射(详见键值对),可以通过键来实现对值的快速访问。
容器接口Collection
容器接口Collection通常不能直接使用,但该接口提供了添加元素、删除元素、管理数据的方法。由于Set接口和List接口都继承了Collection接口,因此这些方法对集合Set与列表List是通用的。下表该出了Collection<E>接口的常用方法,其中的方法默认为public abstract。由于容器框架全部采用泛型实现,使用我们以泛型的形式给出相应的方法,即带类型参数E。
Collection<E>接口的常用方法 | 功能说明 |
---|---|
int size() | 返回容器中元素的个数 |
boolean isEmpty() | 判断容器是否为空 |
boolean contains(Object obj) | 判断容器是否包含元素obj |
boolean add(E element) | 向容器中添加元素element,添加成功返回true;若容器中已包含element,且不允许有重复元素,则返回false |
int hashCode() | 返回容器的哈希码值 |
Object[] toArray() | 将容器转换为数组,返回的数组包含容器的所有元素 |
boolean remove(Object obj) | 从容器中删除元素obj,删除成功返回true;若容器不包含obj,则返回false |
void clear() | 删除容器中的所有元素 |
Iterator<E> iterator() | 返回容器的迭代器 |
boolean equals(Object o) | 比较此collection与指定对象o是否相等 |
void shuffle(List<?> list) | 以随机方式重排list中的元素,类似洗牌 |
boolean containsAll(Collection<?> c) | 判断当前容器是否包含容器c中的所有元素 |
boolean addAll(Collection<? extends E> c) | 集合并运算:将容器c中的所有元素添加到当前容器中 |
boolean removeAll(Collection<?> c) | 集合差运算:在当前容器中删除包含在容器c中的所有元素 |
boolean retainAll(Collection<?> c) | 集合交运算:仅保留当前容器和容器c共同包含的元素 |
列表接口List
列表接口List是Collection子接口,它是一种包含有序元素的线性表,其中的元素必须按顺序存放,且可重复,也可以是空值null。元素之间的顺序关系可以由添加到列表的先后来决定,也可由元素值的大小来决定。List接口使用下标来访问元素。下标范围为0~size()-1。List接口新增了许多方法,使之能够在列表中根据具体位置添加和删除元素。List<E>接口的主要方法如下表所示。方法默认为public abstract。
List<E>接口的常用方法 | 功能说明 |
---|---|
E get(int index) | 返回列表中指定位置的元素 |
E set(int index,E element) | 用元素element取代index位置的元素,返回被取代的元素 |
int indexOf(Object o) | 返回元素o首次出现的序号,若o不存在则返回-1 |
int lastIndexOf(Object o) | 返回元素o最后出现的序号 |
void add(int index,E element) | 在index位置插入元素element |
boolean add(E element) | 在列表的最后添加元素element |
E remove(int index) | 在列表中删除index位置的元素 |
boolean addAll(Collection<? extends E> c) | 在列表的最后添加容器c中的所有元素 |
boolean addAll(int index,Collection<? extends E> c) | 在index位置按照容器c中元素的原有次序插入c中所有元素 |
ListIterator<E> listIterator() | 返回列表中元素的列表迭代器 |
ListIterator<E> listIterator(int index) | 返回从index位置开始的列表迭代器 |
实现List接口的类主要有两个:链表类LinkedList和数组列表类ArrayList。它们都是线性表。
LinkedList链表类采用链表结构保存对象(详见链表),使用循环双向链表实现List。这种结构向链表中任意位置插入、删除元素时不需要移动其他元素,链表的大小是可以动态增大或减小的,但不具有随机存取特性。
ArrayList数组列表类使用一维数组实现List,该类实现的是可变数组,允许所有元素,包括null。具有随机存取特性,插入、删除元素时需要移动其他元素,当元素很多时插入、删除操作的速度较慢。在向ArrayList中添加元素时,其容量会自动增大,但不能自动缩小,但可以使用trimToSize()方法将数组的容量减小到数组列表的大小。
如何选用这两种线性表,通常的原则是:若要通过下标随机访问元素,但除了在末尾处之外,不需要在其他位置插入或删除元素,则应该选择ArrayList类;但若需要在线性表的任意位置上进行插入或删除操作,则应该选择LinkedList类。
下面给出构造方法:
LinkedList<E>类的构造方法 | 功能说明 |
---|---|
public LinkedList() | 创建空的链表 |
public LinkedList(Collection<? extends E> c) | 创建包含容器c中所有元素的链表 |
ArrayList<E>类的构造方法 | 功能说明 |
---|---|
public ArrayList() | 创建初始容量为10的空数组列表 |
public ArrayList(int initialCapacity) | 创建初始容量为initialCapacity的空数组列表 |
public ArrayList(Collection<? extends E> c) | 创建包含容器c中所有元素的数组列表,元素次序与c相同 |
使用线性表时通常声明为List<E>类型,然后通过不同的实现类来实例化列表。如:
List<String> list1=new LinkedList<>();
List<String> list2=new ArrayList<>();
LinkedList<E>类与ArrayList<E>类大部分方法继承自其父类或祖先类,除此之外,它们还各自定义了自己的方法,如下表所示:
LinkedList<E>类的常用方法 | 功能说明 |
---|---|
public void addFirst(E e) | 将元素e插入到列表的开头 |
public void addLast(E e) | 将元素e添加到列表的末尾 |
public E getFirst() | 返回列表中的第一个元素 |
public E getLast() | 返回列表中的最后一个元素 |
public E removeFirst() | 删除并返回列表中的第一个元素 |
pubic E removeLast() | 删除并返回列表中的最后一个元素 |
ArrayList<E>类的常用方法 | 功能说明 |
---|---|
public void trimToSize() | 将ArrayList对象的容量缩小到该列表的当前大小 |
public void forEach(Consumer<? super E> action) | 对action对象执行遍历操作 |
案例:利用LinkedList<E>类构造一个先进后出的栈
public class Test{
private LinkedList<String> Id=new LinkedList<>();//后面尖括号里的参数默认等于前面
//入栈,在开头插入数据
public void push(String name){
Id.addFirst(name);
}
//出栈,也从开头抽取数据
public String pop(){
return Id.removeFirst();
}
//判断是否为空
public boolean isEmpty(){
return Id.isEmpty();
}
//写个主方法测试一下
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
Test test=new Test();
System.out.println("请输入数据(#结束)");
while(true){
String input=scanner.next();
if(input.equals("#")) break;
test.push(input);
}
System.out.print("先进后出的顺序:");
while(!test.isEmpty()){
System.out.print(test.pop()+" ");
}
}
}
输出结果:
请输入数据(#结束)
1 2 3 4 5 #
先进后出的顺序:5 4 3 2 1
对容器中元素进行访问时,经常需要按照某种次序对容器中的每个元素访问且仅访问一次,这就是遍历,也称迭代。遍历是指从容器中获得当前元素的后续元素。对容器的遍历有多种方式。
第1种方式就是利用foreach循环语句,绝大多数的容器都支持该种方式的遍历。下面将上一个案例进行了修改:
for (String o:test.Id) {
System.out.print(o+" ");
}
第2种方式是利用Collection接口中定义的toArray()方法将容器对象转换成数组,然后再利用循环语句对数组中的每个元素进行访问。如下:
Object[] e=test.Id.toArray();//或String[] e= (String[]) test.Id.toArray();
System.out.print("先进后出的顺序:");
for (int i = 0; i < e.length; i++) {
System.out.print(e[i]+" ");
}
第3种方式是利用size()和get()方法进行遍历。即先获取容器内元素的总个数,然后依次取出每个位置上的元素并访问。如下:
for (int i = 0; i < test.Id.size(); i++) {
System.out.print(test.Id.get(i)+" ");
}
第4种方式就是利用Java提供的迭代功能。迭代功能由可迭代接口Iterable和迭代器接口Iterator、ListIterator实现,迭代器是一种允许对容器中元素进行遍历并有选择地删除元素的对象。由于Collection接口声明继承Iterable接口,因此每个实现Collection接口的容器对象都可以调用iterator()方法返回一个迭代器。
Iterator<E>接口的常用方法 | 功能说明 |
---|---|
public abstract bollean hasNext() | 判断是否还有后续元素,若有则返回true |
public abstract E next() | 返回后续元素 |
public abstract void remove() | 删除迭代器当前指向的(即最后被迭代的)元素,即删除由最近一次next()或previous()方法调用返回的元素 |
如下:
Iterator iterator=test.Id.iterator();
while (iterator.hasNext()){
System.out.print(iterator.next()+" ");
}
说明:上述方法都有一定的局限性,容器不一定重写或支持某些方法。
对于容器中元素的遍历次序,接口Iterator支持对List对象从前向后的遍历,但其子接口ListIterator支持对List对象的双向遍历。
ListIterator<E>接口的常用方法 | 功能说明 |
---|---|
public abstract bollean hasPrevious() | 判断是否有前驱元素 |
public abstract E previous() | 返回前驱元素 |
public abstract void add(E e) | 将指定元素插入迭代器指向的位置,当前位置以及之后的全部元素后移一格 |
public abstract void set(E e) | 用元素e替换列表的当前元素 |
public abstract int nextIndex() | 返回基于next()调用的元素序号 |
public abstract int previousIndex() | 返回基于previous()调用的元素序号 |
案例:创建一个数组列表对象并向其添加元素,然后对列表的元素进行修改并遍历。
public class Test{
public static void main(String[] args) {
List<Integer> a1=new ArrayList<>();
for (int i = 1; i < 5; i++) a1.add(i);
System.out.println("数组列表的原始数据"+a1);
ListIterator<Integer> listIterator=a1.listIterator();
listIterator.add(0);
System.out.println("添加数据后数组列表"+a1);
if(listIterator.hasNext()){
int i=listIterator.nextIndex();//index:1
listIterator.next();//返回值为1,同时迭代器指向index:1
listIterator.set(9);
System.out.println("修改数据后数组列表"+a1);
}
listIterator=a1.listIterator(a1.size());//迭代器初始指向index:5
System.out.print("反响遍历数组列表");
while (listIterator.hasPrevious()){
System.out.print(listIterator.previous()+" ");
}
}
}
输出结果:
数组列表的原始数据[1, 2, 3, 4]
添加数据后数组列表[0, 1, 2, 3, 4]
修改数据后数组列表[0, 9, 2, 3, 4]
反响遍历数组列表4 3 2 9 0
集合接口Set
Set是一个不含重复元素的集合接口,它继承自Collection接口,并没有声明其他方法,它的方法都是从Collection接口继承来的。Set集合中的对象不按特定的方式排列,只是简单地把对象加入集合中即可,但加入的对象一定不能重复。集合中元素的顺序与元素加入集合的顺序无关。实现Set接口的两个主要类是哈希集合类HashSet及树集合类TreeSet。
1.哈希集合类HashSet
哈希集合对所包含元素的访问并不是像线性表一样使用下标,而是根据哈希码来存取集合中的元素。假设元素的值为k,代入函数f(x)可以计算得出一个对应值f(k),我们把f(k)就作为元素k的存储位置,只要函数f(x)选取得当,我们就能确保大部分元素都能找到自己独特的存储位置,这样存储数据就能实现快速查找。函数f(x)叫做哈希函数,f(k)叫做哈希码,按照这种关系建立的表称为哈希表,也称散列表。(详见数据结构相关内容)
HashSet集合类是基于哈希表的Set接口实现的。HashSet根据哈希码来确定元素在集合中的存储位置(即内存地址),因此可以根据哈希码来快速地找到集合中的元素。HashSet集合不保证迭代顺序(显然),但允许元素值为null。在比较两个加入哈希集合HashSet中的元素是否相同时,会先比较哈希码方法hashCode()的返回值是否相同,若相同则再使用equals()方法比较其存储位置(即内存地址),若两者都相同则视为相同的元素。之所以在比较了哈希码之后,还要通过equals()方法进行比较,是因为对不同元素计算出的哈希码可能相同。因此,对于哈希集合来说,若重写了元素对应类的equals()方法或hashCode()方法中的某一个,则必须重写另一个,以保证其判断的一致性。
HashSet<E>集合类的构造方法 | 功能说明 |
---|---|
public HashSet() | 创建默认初始容量是16,默认上座率为0.75的空哈希集合 |
public HashSet(int initialCapacity) | 创建初始容量是initialCapacity,默认上座率为0.75的空哈希集合 |
public HashSet(int initialCapacity,float loadFactor) | 创建初始容量是initialCapacity,默认上座率为loadFactor的空哈希集合 |
public HashCode(Collection<? extends E> c) | 创建包含容器c中所有元素,默认上座率为0.75的哈希集合 |
HashSet<E>集合类的常用方法 | 功能说明 |
---|---|
public boolean add(E e) | 如果集合中尚未包含指定元素,则添加元素e并返回true;如果集合中已包含该元素,则该调用不更改集合并返回false |
public void clear() | 删除集合中的所有元素,集合为空 |
public boolean contains(Object o) | 如果集合中包含元素o,则返回true |
public int size() | 返回集合中所包含元素的个数,即返回集合的容量 |
案例:将一组字符串存储到哈希集合中,输出重复的元素,最后输出集合大小以及集合元素
public class Test{
public static void main(String[] args) {
HashSet<String> hashSet=new HashSet<>();
Scanner scanner=new Scanner(System.in);
while (scanner.hasNext()){
String str=scanner.next();
if(str.equals("#"))break;//输入#时退出
if(!hashSet.add(str))//输出重复的元素
System.out.println("元素"+str+"重复");
}
System.out.println("集合的容量为:"+hashSet.size());
System.out.print("各元素为:");
Iterator iterator=hashSet.iterator();
while (iterator.hasNext()){
System.out.print(iterator.next()+" ");
}
}
}
输出结果:
I come I see I go #
元素I重复
元素I重复
集合的容量为:4
各元素为:see go I come
说明:输出哈希集合元素时并不一定是按照元素的存储顺序输出的,因为哈希集合中的元素是没有特定顺序的,若一定要让元素有序输出,则需要使用LinkedHashSet类。
2.树集合类TreeSet
树集合类TreeSet不仅实现了Set接口,还实现了java.util.SortedSet接口。TreeSet的工作原理与HashSet相似,但TreeSet增加了一个额外步骤,以保证集合中的元素总是处于有序状态。因此,当排列很重要时,就选择TreeSet,否则应选用HashSet。TreeSet<E>类的大多数方法继承自其父类或祖先类。
TreeSet<E>类的构造方法 | 功能说明 |
---|---|
public TreeSet() | 创建新的空树集合,其元素按自然顺序进行排序 |
public TreeSet(Collection<? extends E> c) | 创建包含容器c元素的新TreeSet,按其元素的自然顺序进行排序 |
TreeSet<E>类新增的方法 | 功能说明 |
---|---|
public E first() | 返回集合中的第一个(最低)元素 |
public E last() | 返回集合中的最后一个(最高)元素 |
public SortedSet<E> headSet(E toElement) | 返回一个新集合,新集合元素是toElement(不包含toElement)之前的所有元素 |
public SortedSet<E> tailSet(E fromElement) | 返回一个新集合,新集合元素包含fromElement及fromElement之后的所有元素 |
public SortedSet<E> subSet(E fromElement,E toElement) | 返回一个新集合,新集合元素包含从fromElement到toElement(不包含toElement)之间的所有元素 |
public E lower(E e) | 返回严格小于给定元素e的最大元素,如果不存在这样的元素,则返回null |
public E higher(E e) | 返回严格大于给定元素e的最小元素,如果不存在这样的元素,则返回null |
public E floor(E e) | 返回严格小于或等于给定元素e的最大元素,如果不存在这样的元素,则返回null |
public E ceiling(E e) | 返回严格大于或等于给定元素e的最小元素,如果不存在这样的元素,则返回null |
案例:先创建一个哈希集合对象hs,并向其添加元素,然后再利用hs创建树集合对象ts,之后利用树集合的相应方法输出某些元素。
public class Test{
public static void main(String[] args) {
HashSet<String> hs=new HashSet<>();
hs.add("C");
hs.add("C++");
hs.add("C#");
hs.add("Java");
hs.add("Python");
TreeSet<String> ts=new TreeSet<>(hs);//利用hs创建树集合对象ts
System.out.println("树集合:"+ts);
System.out.println("树集合的第一个元素:"+ts.first());
System.out.println("树集合的最后一个元素:"+ts.last());
System.out.println("headSet(C#)的元素:"+ts.headSet("C#"));
System.out.println("tailSet(C#)的元素:"+ts.tailSet("C#"));
System.out.println("ceiling(J)的元素:"+ts.ceiling("J"));
}
}
输出结果:
树集合:[C, C#, C++, Java, Python]
树集合的第一个元素:C
树集合的最后一个元素:Python
headSet(C#)的元素:[C]
tailSet(C#)的元素:[C#, C++, Java, Python]
ceiling(J)的元素:Java
映射接口Map
Map是另一种存储数据结构的对象,Map接口与List接口和Set接口有明显的区别。Map中的元素都是成对出现的,它提供了键(key)到值(value)的映射。值是指要存入Map中的元素(对象),在将元素存入Map对象时,需要同时给定一个键,这个键决定了元素在Map中的存储位置。一个键和它对应的值构成一个条目,真正在Map中存储的是这个条目。键很像下标,但在List中下标是整数,而在Map中键可以是任意类型的对象。如果要在Map中检索一个元素,必须提供相应的键,这样就可以通过键访问到其对应元素的值。Map中的每个键都是唯一的,且每个键最多只能映射到一个值。由于Map中存储元素的形式较为特殊,所以Map没有继承Collection接口。下表给出Map<K,V>接口的常用方法,其中K表示键的类型,V表示值的类型。因为Map是接口,所以其方法默认为public abstract。
Map<K,V>接口的常用方法 | 功能说明 |
---|---|
V put(K key,V value) | 以key为键,向集合中添加值为value的元素,其中key必须唯一,否则新添加的值会取代已有的值 |
void putAll(Map<? extends K,? extends V> m) | 将映射m中的所有映射关系复制到调用此方法的映射中 |
boolean containsKey(Object key) | 判断是否包含指定的键key |
boolean containsValue(Object value) | 判断是否包含指定的值value |
V get(Object key) | 返回键key所映射的值,若key不存在则返回null |
Set<K> keySet() | 返回该映射中所有键对象形成的Set集合 |
Collection<V> values() | 返回该映射中所有值对象形成的Collection集合 |
V remove(Object key) | 将键为key的条目,从Map对象中删除 |
Set<Map.Entry<K,V>> entrySet() | 返回映射中的“键—值”对的集合 |
映射接口Map常用的实现类有哈希映射HashMap和树映射TreeMap。HashMap映射是基于哈希表的Map接口实现的类,所以HashMap通过哈希码对其内部的映射关系进行快速查找,因此对于添加和删除映射关系效率较高,并且允许使用null键和null值,但必须保证键的唯一性;而树映射TreeMap中的映射关系存在一定的顺序,如果希望Map映射中的元素也存在一定的顺序,应该使用TreeMap类实现的Map映射,由于TreeMap类实现的Map映射中的映射关系是根据键对象按照一定的顺序排列的,因此不允许键对象是null。
HashMap映射的方法大多数是继承自Map接口,因此下表只给出HashMap<K,V>映射常用的构造方法。
HashMap<K,V>映射常用的构造方法 | 功能说明 |
---|---|
public HashMap() | 构造一个具有默认初始容量(16)和默认上座率(0.75)的空HashMap对象 |
public HashMap(int initialCapacity) | 构造一个初始容量为initialCapacity和默认上座率(0.75)的空HashMap对象 |
public HashMap(Map<? extends K,? extends V> m) | 创建一个映射关系与指定Map相同的新HashMap对象。具有默认上座率(0.75)和足以容纳指定Map中映射关系的初始容量 |
TreeMap<K,V>映射的构造方法 | 功能说明 |
---|---|
public TreeMap() | 使用键的自然顺序创建一个新的空树映射 |
public TreeMap(Map<? extends K,? extends V> m) | 创建一个与给定映射具有相同映射关系的新树映射,该映射根据其键的自然顺序进行排序 |
TreeMap<K,V>映射的常用方法 | 功能说明 |
---|---|
public K firstKey() | 返回映射中的第一个(最低)键 |
public K lastKey() | 返回映射中的最后一个(最高)键 |
public SortedMap<K,V> headMap(K toKey) | 返回键值小于toKey的那部分映射 |
public SortedMap<K,V> tailMap(K fromKey) | 返回键值大于或等于fromKey的那部分映射 |
public K lowerKey(K key) | 返回严格小于给定键key的最大值,如果不存在这样的键,则返回null |
public K floorKey(K key) | 返回小于或等于给定键key的最大值,如果不存在这样的键,则返回null |
public K higherKey(K key) | 返回严格大于给定键key的最小值,如果不存在这样的键,则返回null |
public K ceilingKey(K key) | 返回大于或等于给定键key的最小值,如果不存在这样的键,则返回null |
案例:创建一个哈希映射类HashMap对象,并向其添加若干个元素后,删除其中的某元素,之后再创建一个树映射类TreeMap的对象,并将HashMap对象中的元素添加其中,然后分别遍历由HashMap与TreeMap类实现的Map映射。
public class Test{
public static void main(String[] args) {
Map<String,String> hm=new HashMap();
hm.put("005","人工智能");
hm.put("001","计算机科学与技术");
hm.put("003","软件工程");
hm.put("004","物联网工程");
hm.put("002","信息安全");
System.out.println("哈希映射中的映射如下:\n"+hm);
String str=hm.remove("005");//在hm中删除键值为"005"的元素
Set keys=hm.keySet();//获取hm的键的对象的集合
Iterator it1= keys.iterator();//获取键对象集合keys的迭代器
System.out.println("HashMap类实现的Map映射,无序:");//与输入顺序不一致
while (it1.hasNext()){
String key=(String)it1.next();
String value=hm.get(key);
System.out.println(key+" "+value);
}
TreeMap<String,String> tm=new TreeMap<>();
tm.putAll(hm);
Iterator it2=tm.keySet().iterator();
System.out.println("TreeMap类实现的Map映射,键值升序:");
while (it2.hasNext()){
String key=(String)it2.next();
String value=tm.get(key);
System.out.println(key+" "+value);
}
}
}
输出结果:
哈希映射中的映射如下:
{001=计算机科学与技术, 002=信息安全, 003=软件工程, 004=物联网工程, 005=人工智能}
HashMap类实现的Map映射,无序:
001 计算机科学与技术
002 信息安全
003 软件工程
004 物联网工程
TreeMap类实现的Map映射,键值升序:
001 计算机科学与技术
002 信息安全
003 软件工程
004 物联网工程
本章小结
- 在定义类、接口和方法时若指定了“类型参数”,则分别称为泛型类、泛型接口和泛型方法。
- 用泛型类创建的泛型对象就是在泛型类体内的每个类型参数T处分别用某个具体的实际类型替代,这个过程称为泛型实例化,利用泛型类创建的对象称为泛型对象。
- 在创建泛型类对象的过程中,实际类型必须是引用类型,而不能用基本类型。
- 泛型方法与其所在的类是否是泛型类没有关系。
- 在调用泛型方法时,可以将实际类型放在尖括号内作为方法名的前缀。
- 泛型方法的返回值类型和至少一个参数类型应该是泛型,而且类型应该是一致的。泛型方法广泛应用在方法返回值和参数均是容器类对象的情况。
- 泛型方法与泛型类在传递类型参数方面的一个重要区别是:对于泛型方法,不需要把实际的类型传递给泛型方法;但泛型类却恰恰相反,即必须把实际的类型参数传递给泛型类。
- 虽然泛型的类型参数代表一种数据类型,但不能使用泛型的类型参数创建对象。
- 在泛型中可以用类型参数声明一个数组,但不能使用类型参数创建数组对象。
- 不能在静态环境中使用泛型类的类型参数。泛型类中的静态方法不可以使用泛型类中已经声明过的类型参数。如果静态方法操作的引用数据类型不确定的时候,即如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法。
- 异常类不能是泛型的,即在异常类中不能使用泛型的类型参数。
- 在定义泛型类或使用泛型类创建对象时,对泛型的类型做出限制称为泛型限制。
- 泛型类的通配符有三种形式:第1种是"?",它等价于"? extends Object",称为非受限通配;第2种是"? extends T",表示T或T的一个未知子类型,称为上限通配;第3种是"? super T",表示T或T的一个未知父类型,称为下限通配。
- 当方法中的多个参数之间或参数与返回值之间存在类型依赖关系时,则应选用泛型方法。如果方法中不存在类型之间的依赖关系,则应选用通配符。
- 容器是存储对象的数据结构的集合。容器框架中定义的所有接口和类都存储在java.util包中。
- 从容器的当前元素获取其后续元素进行访问的过程称为迭代,迭代也称遍历。
- List的对象用于存储一个由元素构成的线性表;Set的对象是存储一组不重复元素的集合;Map的对象保持了键到值的映射。
- List是一种包含有序元素的线性表,其中的元素必须按顺序存放,且可重复,也可以是空值null。实现List接口的类主要有链表类LinkedList和数组列表类ArrayList。
- LinkedList是实现List接口的链表类,采用双向链表结构保存元素,访问元素的时间取决于元素在表中所处的位置,但对链表的增长或缩小则没有任何额外的开销。
- ArrayList是实现List接口的数组列表类,它使用一维数组实现List,支持元素的快速访问,但在数组的扩展或缩小时则需要额外的系统开销。
- Set是一个不含重复元素的集合接口。实现Set接口的两个主要类是哈希集合类HashSet及树集合类TreeSet。
- HashSet的工作原理是在哈希集合中元素的“值”与该元素的存储位置之间建立起一种映射关系,这种映射关系称为哈希函数或散列函数,有哈希函数计算出来的数值称为哈希码或散列索引。虽然HashSet中的元素是无序的,但由于HashSet特性还是可以快速地添加或访问其中的元素。
- 因为对不同元素计算出的哈希码可能相同,所以判断哈希集合中的元素是否相同时需要同时使用hashSet()方法和equals()方法。
- TreeSet类对象中的元素总是有序的,所以当插入元素时需要一定的开销。
- Map中的元素都是成对出现的,它提供了键(key)到值(value)的映射。
- 映射接口Map常用的实现类有HashMap和TreeMap。HashMap类与TreeMap类的关系如同HashSet与TreeSet的关系一样。
- HashMap类是基于哈希表的Map接口的实现,允许使用null键和null值,但必须保证键的唯一性,HashMap是无序的。
- TreeMap类中的映射关系存在一定的顺序,不允许键对象是null。TreeMap是有序的。
课后习题
- 将整数1~10存放到一个线性表LinkedList的对象中,然后将其下标为4的元素从列表中删除。
- 利用ArrayList类创建一个对象,并向其添加若干个字符串型元素,然后随机选一个元素输出。
- 已知集合A={1,2,3,4}和B={1,3,5,7,9,11},编程求A与B的交集、并集和补集。
- 利用随机函数生成10个随机数,并将它们存入到一个HashSet对象中,然后利用迭代器输出。
- 利用随机函数生成10个随机数,并将它们有序地存入到一个TreeSet对象中,然后利用迭代器输出。
- 利用HashMap类对象存储公司电话号码簿,其中包含公司名称和公司的电话号码,然后进行删除一个公司和查询一个公司电话号码的操作。