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集合的迭代。