JAVA集合LIST MAP SET详解

时间:2021-10-07 04:27:11

1. 集合框架介绍

我们知道,计算机的优势在于处理大量的数据,在编程开发中,为处理大量的数据,必须具备相应的存储结构,之前学习的数组可以用来存储并处理大量类型相同的数据,但是通过上面的课后练习,会发现数组在应用中的限制:数组长度一旦确定,就无法更改;除非采用建立新数组,再将原数组内容拷贝过来;数组中只能存放指定类型的数据,操作不方便。在实际开发中,为了操作方便,JDK中提供了List集合。

List集合与数组的用途非常相似,都是用来存储大量数据的,不同处有两点:

1. 数组长度在使用前必须确定,一旦确定不能改变。而List集合长度可变,无需定义。

2. 数组中必须存放同一类型的数据,List集合中可以存放不同类型的数据。

List集合是Java集合框架中的一种,另外两种集合Set和Map会在下面介绍。List集合在JDK中被封装称为接口,针对List接口,有若干种实现,常用的有三个子类,即ArrayList、Vector和LinkedList。这三个类的功能与用法相同,但内部实现方式不同。下面以ArrayList为例介绍集合的常用操作,Vector和LinkedList的使用方法与ArrayList类似。

数组与List集合的常规操作类似,下面通过代码对比两者的用法:

代码演示:数组的基本操作

public class ArrayDemo {

public static void main(String[] args) {

String[] array = new String[3];

for (int i = 0; i < 3; i++) {

array[i] = "Hello";

}

String a = array[0];

}

}

代码演示:List集合的基本操作

import java.util.ArrayList;    ①

public class ListDemo {

public static void main(String[] args) {

ArrayList list = new ArrayList();    ②

for (int i = 0; i < 3; i++) {

list.add("Hello");    ③

}

String a = (String)list.get(0);    ④

}

}

代码解析:

① 集合框架在java.util包中,使用前必须使用import语句引入对应的类。

② List集合的定义时不需要指定大小,也不用指定集合中保存的数据类型。

③ 向List集合中添加数据时不需要制定下标,List集合会自动生成下标。

④ 获取List集合的元素时使用get方法并传入下标,然后强制类型转换为实际类型。

代码演示:使用集合记录学员姓名

public static void main(String[] args) {

System.out.println("请输入班级学员姓名,输入OVER结束");

java.util.Scanner scanner = new java.util.Scanner(System.in);

ArrayList list = new ArrayList();

do {

String name = scanner.next();

if (name.equalsIgnoreCase("OVER")) break;

list.add(name);

} while (true);

System.out.println(list);    ①

}

代码解析:

① List集合重写了toString方法,可以将集合中的元素依次输出。

2. List集合的常用方法

下表列出了List集合的常用方法:

返回类型

方法名称

说明

boolean

add(Object obj)

加入元素,返回是否添加成功

boolean

clear()

清除集合中的元素

boolean

contains(Object obj)

查找集合中是否存在传入的元素

Object

get(int index)

获取指定位置的元素

boolean

isEmpty()

判断集合是否为空

Object

remove(int index)

删除制定位置的元素,并返回该元素

int

size()

获取集合大小

Object[]

toArray()

将集合转换成一个数组

表:  List集合的常用方法

下面通过实例演示各个方法的用途:

代码演示:List集合的常用方法

import java.util.ArrayList;

public class Demo {

public static java.util.Scanner scanner = new java.util.Scanner(System.in);

public static void main(String[] args) {

ArrayList listA = new ArrayList();

ArrayList listB = new ArrayList();

System.out.println("请输入A班学员姓名,输入OVER结束");

inputName(listA);

System.out.println("请输入B班学员姓名,输入OVER结束");

inputName(listB);

//合并集合listA与listB

listA.addAll(listB);

System.out.println("请输入要查找的学员姓名");

String name = scanner.next();

int pos = listA.indexOf(name);

if (pos==-1) {

System.out.println("没有找到");

} else {

System.out.println("找到了,位置是:" + pos);

}

System.out.println("请输入要删除的学员姓名");

String delName = scanner.next();

if (listA.remove(delName)) {

System.out.println("删除成功!");

} else {

System.out.println("没有该学员");

}

}

public static void inputName(ArrayList list) {

do {

String name = scanner.next();

if (name.equalsIgnoreCase("OVER")) break;

list.add(name);

} while (true);

}

}

3. 使用List集合保存对象

使用List集合保存对象时,主要注意以下几点:

1. 集合中保存的是对象的引用,观察以下代码:

代码演示:使用集合保存对象

import java.util.ArrayList;

class Student {

String name;

int age;

public Student(String name, int age) {

this.name = name;

this.age = age;

}

public String toString() {

return name + "/" + age;

}

}

public class Demo {

public static void main(String[] args) {

ArrayList list = new ArrayList();

Student stu = new Student("Tom" , 10);

for (int i = 0; i < 3; i++) {

stu.age = 10 + i;

list.add(stu);

}

System.out.println(list);

}

}

上面代码的原意是在集合中保存三个Student对象,age分别为10、11、12,但实际输出的age值均为12。这是因为list集合中保存的是stu对象的引用,而在循环中stu的引用并没有变化,所以循环结束后集合中的三个元素都指向stu对象,age的值自然也是最后的12。将代码“Student stu = new Student("Tom" , 10);”放入循环内可以解决这一问题。

2. 使用remove、contains、indexOf等方法时,应该重写类的equals方法,观察以下代码:

代码演示:未重写equals方法的代码

//省略了Student类的定义

public class Demo {

public static void main(String[] args) {

ArrayList list = new ArrayList();

list.add(new Student("Tom" , 11));

list.add(new Student("Jerry" , 22));

list.add(new Student("Alice" , 33));

System.out.println(list.contains(new Student("Tom" , 11)));

System.out.println(list.indexOf(new Student("Jerry" , 22)));

System.out.println(list.remove(new Student("Alice" , 33))?"成功":"无此项");

}

}

在上例中,我们希望判断学员Tom是否存在,查找学员Jerry,删除学员Alice,但是输出的结果却是不存在,找不到,删不掉。这是因为List集合会调用元素的equals方法来判断对象是否相等,而Student类没有重写equals方法,默认是按引用地址比较,而每个学员对象的地址又不相同,所以出现这个现象。通过给Student类添加equals方法可以解决这个问题:

代码演示:重写equals方法后的Student类

class Student {

String name;

int age;

public Student(String name, int age) {

this.name = name;

this.age = age;

}

public boolean equals(Object obj) {

if (obj == null) return false;

if (!(obj instanceof Student)) return false;

Student stu = (Student) obj;

return stu.name.equals(this.name) && stu.age == this.age;

}

}

4. 三种List集合的比较

我们说过,ArrayList、Vector与LinkedList的使用方法相同,内部实现方式不同。而内部实现方式的不同又决定了三种集合的适用范围,了解三种集合的内部实现,才能正确的选择使用类型。

? ArrayList与Vector比较

ArrayList与Vector的内部实现类似,Vector设计为线程安全,ArrayList设计为非线程安全。为了保证线程安全,Vector在性能方面稍逊于ArrayList,目前我们编写的都是单线程应用程序,应选择使用ArrayList。

? ArrayList与LinkedList

ArrayList与LinkedList均设计为非线程安全,ArrayList内部采用数组实现(与Vector相同),LinkedList内部采用链表结构实现。

ArrayList采用数组保存元素,意味着当大量添加元素,数组空间不足时,依然需要通过新建数组、内存复制的方式来增加容量,效率较低;而当进行对数组进行插入、删除操作时,又会进行循环移位操作,效率也较低;只有进行按下标查询时(get方法),使用数组效率很高。

LinkedList采用链表保存元素,在添加元素时只需要进行一次简单的内存分配即可,效率较高;进行插入、删除操作时,只需对链表中相邻的元素进行修改即可,效率也很高;但进行按下标查询时,需要对链表进行遍历,效率较低。下图演示了链表结构的特性:

图:  链表结构,每个元素引用后面的元素

图:  向链表中插入元素,只需修改两处引用

图:  删除链表中的元素,也只需要修改两处引用

可以总结出ArrayList在进行数据的新增、插入、删除时效率较低,按下标对数据进行查找时效率较高;LinkedList正好相反。一般来说ArrayList保存经常进行查询操作的集合,LinkedList适用于保存经常进行修改操作的集合。

5. 章节概述

1. List集合与数组的区别。

2. List集合实际上包含了3个常用的集合类,即ArrayList、Vector和LinkedList。

3. List集合的常用操作。

4. ArrayList采用数组保存元素,意味着当大量添加元素,数组空间不足时,依然需要通过新建数组、内存复制的方式来增加容量,效率较低;而当进行对数组进行插入、删除操作时,又会进行循环移位操作,效率也较低;只有进行按下标查询时(get方法),使用数组效率很高。

5. ArrayList与Vector的内部实现类似,Vector设计为线程安全,ArrayList设计为非线程安全。为了保证线程安全,Vector在性能方面稍逊于ArrayList,目前我们编写的都是单线程应用程序,应选择使用ArrayList。

6. ArrayList与LinkedList均设计为非线程安全,ArrayList内部采用数组实现(与Vector相同),LinkedList内部采用链表结构实现。

7. LinkedList采用链表保存元素,在添加元素时只需要进行一次简单的内存分配即可,效率较高;进行插入、删除操作时,只需对链表中相邻的元素进行修改即可,效率也很高;但进行按下标查询时,需要对链表进行遍历,效率较低。

1. Map集合

在上面讲的List集合中,可用通过List集合提供的各种方法来对其中的元素进行操作,从而可以方便用户操作,但是如果要从List集合中获取一个特定的对象,操作是比较繁琐的。

在类Person中有cardId和name两个属性,分别代表编号和姓名,创建两个Person对象并存储到ArrayList集合中 ,如果要从集合中获取指定的对象,则必须要通过迭代整个集合来获得,如下所示:

代码演示:Person类

public class Person {

String cardId;

String name;

public Person(String cardId, String name) {

this.cardId = cardId;

this.name = name;

}

}

代码演示:从ArrayList中获取特定的对象

public class ArrayListTest {

public static void main(String[] args) {

Person personA = new Person("001", "Tom");

Person personB = new Person("002", "Jack");

ArrayList list = new ArrayList();

list.add(personA);

list.add(personB);

for (int i = 0; i < list.size(); i++) {

Person person = (Person) list.get(i);

if (person.cardId.equals("002")) {

System.out.println(person.name);

}

}

}

}

从上面的示例中,我们看到从list集合中获取一个对象的繁琐,有没有简单的方法呢?在JDK中专门提供了Map集合来存储上面这种一对一映射关系的对象。

Map集合用于保存具有映射关系的数据,即以键值对(key->value)的方式来存储数据。因此在Map集合内部有两个集合,一个集合用于保存Map中的key(键),一个集合用于保存Map中的value(值),其中key和value可以是任意数据类型数据。

图:  Map集合

Map集合中的常用类有Hashtable和HashMap,两个类的功能和用法相似,下面以HashMap为例介绍Map集合的用法。

代码演示:Map集合使用

public class MapTest {

public static void main(String[] args) {

HashMap map = new HashMap();  ①

map.put("001", "Tom");  ②

map.put("002", "Jack");

String name = (String) map.get("002");  ③

System.out.println(name);

}

}

代码解析:

① 创建HashMap对象。

② 利用HashMap中的put方法将键值对形式的对象进行存储,put方法中的第一个参数为映射关系中key的值,put方法的第二个参数为映射关系中value的值。

③ 利用HashMap的get方法获取key对应的value,然后强制类型转换为实际类型。get中的参数为key,返回值为key对应的value。

下表列出了HashMap中常用的方法:

返回类型

方法名称

作用

Object

put(Object key,Object value)

加入元素,返回与此key关联的原有的value,不存在则返回null

void

clear()

从集合中移除所有的元素

boolean

containsKey(Object key)

根据key从集合中判断key是否存在

boolean

containsValue(Object value)

根据value从集合中判断value是否存在

Object

get(Object key)

根据key返回key对应的值

Set

keySet()

返回Map集合中包含的键集合

Object

remove(Object key)

从集合中删除key对应的元素,返回与key对应的原有value,不存在则返回null

int

size()

返回集合中的元素的数量

表:  HashMap常用方法

Map集合的综合示例:

代码演示:Map集合综合演示

import java.util.HashMap;

import java.util.Scanner;

public class TestMap {

public static void main(String[] args) {

HashMap map = new HashMap();

Scanner scanner = new Scanner(System.in);

for (int i = 0; i < 5; i++) {

System.out.println("请输入身份证号:");

String id = scanner.next();

System.out.println("请输入姓名:");

String name = scanner.next();

map.put(id, name);  ①

}

int size = map.size();  ②

System.out.println("数据输入完毕!共" + size

+ "条数据!\n---------------------------------");

String answer = "no";

do {

System.out.println("请输入你要查找的用户的身份证号:");

String id = scanner.next();

if (map.containsKey(id)) {  ③

String name = (String) map.get(id);  ④

System.out.println("您查找的用户姓名为:" + name);

} else {

System.out.println("您查找的用户不存在!");

}

System.out.println("您还要继续查找吗?(yes/no)");

answer = scanner.next();

} while ("yes".equalsIgnoreCase(answer));

System.out.println("请输入要删除的用户的身份证号:");

String id = scanner.next();

if (map.containsKey(id)) {

String name = (String) map.remove(id);  ⑤

System.out.println("用户" + name + "删除成功!");

} else {

System.out.println("您要删除的用户不存在!");

}

System.out.println("要格式化系统吗?(yes/no)");

String format = scanner.next();

if ("yes".equalsIgnoreCase(format)) {

map.clear();  ⑥

System.out.println("系统格式化完毕!当前系统中数据为"

+ map.size() + "条");

}

System.out.println("程序运行结束!");

}

}

代码解析:

① 使用put方法将身份证号和姓名存入Map集合中。

② 使用size方法获得集合中的映射关系条数。

③ 使用containsKey方法判断集合是否存在与key对应的映射关系。

④ 使用get方法获得身份证号对应的姓名。

⑤ 使用remove方法删除身份证号对应的用户,返回身份证号对应的姓名。

⑥ 使用clear方法删除Map集合中所有的映射关系。

来看下面的一个示例:

代码演示:Map集合中重复key

import java.util.HashMap;

public class DemoMap {

public static void main(String[] args) {

HashMap map = new HashMap();

map.put("001", "小美");

map.put("002", "阿聪");  ①

map.put("002", "小莉");  ②

String name = (String) map.get("002");

System.out.println(name);

}

}

注意①和②处的代码,在向集合中添加值的时候,使用了重复的key,但是value不同,在下面获得key“002”的value为多少呢?程序运行的结果是“小莉”。从结果可以中可以知道,Map集合中的key不能是重复的,如果重复,那么后面添加的映射关系会覆盖前面的映射关系。导致这样情况的出现主要是因为Map集合中的key的维护是依靠Set集合(马上会学习到)完成的。

HashMap和Hashtable的操作是相同的,他们的区别如下:

? Hashtable是线程安全的,HashMap是非线程安全的。所有HashMap比Hashtable的性能更高。

? Hashtable不允许使用使用null值作为key或value,但是HashMap是可以的。

2. Set集合

Set集合和List集合的很多的用法是相同的。但是Set集合中的元素是无序的,元素也是不能重复的。Set集合中常用类为HashSet。

HashSet类中常用的方法如下:

返回类型

方法名称

作用

boolean

add(Object obj)

加入元素

void

clear()

移除Set集合中所有元素

boolean

contains(Object obj)

判断Set集合中是否包含指定元素

boolean

isEmpty()

判断Set集合是否为空

Iterator

iterator()

返回Set集合中对元素迭代的迭代器

boolean

remove(Object obj)

从集合中删除元素

Int

size()

返回集合中的元素数量

表:  HashSet类常用方法

通过上面的表,可以清楚的看到Set集合的用法和List集合是相似的,但是需要注意Set集合的迭代和List集合是不同的,List的集合的迭代可以通过for循环获得索引来进行,但是Set集合的迭代必须要通过迭代器进行。

代码演示:Set集合的迭代

import java.util.HashSet;

import java.util.Iterator;

public class SetIterator {

public static void main(String[] args) {

HashSet set = new HashSet();

set.add("a");

set.add("b");

set.add("c");

Iterator iter = set.iterator();  ①

while (iter.hasNext()) {  ②

String str = (String) iter.next();  ③

System.out.println(str);

}

}

}

代码解析:

① 通过Set集合的iterator()方法获得该集合的迭代器,迭代器是Iterator类的实例。

② 根据迭代器的hasNext()方法判断集合中是否还有元素,如果有就返回true。

③ 根据迭代器的next()方法获得集合中的元素,并强制类型转换为目标类型。

从上面例子的运行结果,可以看出Set中的元素是无序的。通过Set集合的迭代再来学习Map集合的迭代。

代码演示:Map集合的迭代

import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;

import java.util.Set;

public class MapIter {

public static void main(String[] args) {

HashMap map = new HashMap();

map.put("001", "小美");

map.put("002", "阿聪");

map.put("003", "小黑");

HashSet keys = (HashSet) map.keySet();  ①

Iterator iter = keys.iterator();

while (iter.hasNext()) {

String key = (String) iter.next();  ②

String value = (String) map.get(key);  ③

System.out.println(key + ":" + value);

}

}

}

代码解析:

① 调用Map集合的keySet方法获得Map集合中的key的集合。

② 获得key。

③ 根据key获得对应的value。

下面通过一个示例演示Set集合中不允许元素重复的特性:

代码演示:向Set集合中添加重复元素

import java.util.HashSet;

import java.util.Iterator;

public class Demo2 {

public static void main(String[] args) {

HashSet set = new HashSet();

set.add("a");

set.add("a");

set.add("c");

System.out.println("集合长度为:" + set.size());

Iterator iter = set.iterator();

while (iter.hasNext()) {

String str = (String) iter.next();

System.out.println(str);

}

}

}

上面的代码输出集合的长度为2,而且集合中的元素只有一个a和c,从结果中可以看出Set集合中的元素是不能重复的。带着这个结论再来看下面的示例:

代码演示:向Set集合中添加重复元素

import java.util.HashSet;

import java.util.Iterator;

public class Demo3 {

public static void main(String[] args) {

Person personA = new Person("001", "Tom");

Person personB = new Person("001", "Tom");

HashSet set = new HashSet();

set.add(personA);

set.add(personB);

System.out.println("集合中元素个数:" + set.size());

Iterator iter = set.iterator();

while (iter.hasNext()) {

Person p = (Person) iter.next();

System.out.println(p.cardId + ":" + p.name);

}

}

}

程序的运行结果如下:

集合中元素个数:2

001:Tom

001:Tom

发现程序的运行结果是有“问题”的,因为Set集合中是不允许存放重复的元素的,但是两个Person对象的属性值是完全相同的,怎么还都能存放进去呢?要找到问题的答案,需要了解下Set集合的存放原理。

图:  Set集合存储

从上图中可以看到,Set集合中的元素的无序性,但是Set集合是怎么判断每个元素的存放的位置呢?在向Set集合中存放元素时,Set集合根据元素的hashCode()方法来获取一个int类型的数据,然后根据这个数据来计算元素在集合中的位置。但是在存储元素时会出现两个元素的hashCode()方法返回值相同的情况,比如上图的对象C和D就出现了这种情况,导致计算出元素在集合中的位置相同,这种情况称之为“冲突”。如果发生了冲突,Set集合会根据发生冲突元素之间调用equals()方法进行比较,如果equals()返回值为true,说明两个元素为相同的元素,这样会导致添加操作无效。如果equals()返回值为false,说明两个元素不相同,这样Set集合会将该元素进行偏移存储。“冲突”发生的频率越高,Set集合的性能就越低,要尽可能的避免冲突的发生, 就要在类中重写hashCode()方法,并且要尽可能的保证hashCode()方法返回值是唯一的。在重写hashCode()方法时有个技巧,就是让对象中的数值属性和一个素数相乘,并将积相加,对于对象类型,调用其hashCode()方法即可。

对于hashCode()和equals()两个方法有这样的规律,hashCode()方法返回值相同时,equals()方法比较并不一定相等,但是equals()方法比较相等,hashCode()方法返回值是相同的。

代码演示:实现Person对象的equals和hashCode方法

public class Person {

String cardId;

String name;

public Person(String cardId, String name) {

this.cardId = cardId;

this.name = name;

}

public int hashCode() {

return cardId.hashCode() + name.hashCode();

}

public boolean equals(Object obj) {

if (obj == null){

return false;

}

if(obj instanceof Person){

Person other = (Person) obj;

if (cardId == null) {

if (other.cardId != null){

return false;

}

} else if (!cardId.equals(other.cardId)){

return false;

}

if (name == null) {

if (other.name != null){

return false;

}

} else if (!name.equals(other.name)){

return false;

}

}else{

return false;

}

return true;

}

}

再次运行Demo3的代码,运行结果如下:

集合中元素个数:1

001:Tom

3. 本章总结

? Map集合用于保存具有映射关系的数据,即以键值对(key->value)的方式来存储数据。因此在Map集合内部有两个集合,一个集合用于保存Map中的key(键),一个集合用于保存Map中的value(值),其中key和value可以是任意数据类型数据。

? Set集合的特点。

? Set集合的使用及迭代。

? Map集合的迭代。