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的使用方法相同,内部实现方式不同。而内部实现方式的不同又决定了三种集合的适用范围,了解三种集合的内部实现,才能正确的选择使用类型。
1.ArrayList与Vector比较
ArrayList与Vector的内部实现类似,Vector设计为线程安全,ArrayList设计为非线程安全。为了保证线程安全,Vector在性能方面稍逊于ArrayList,目前我们编写的都是单线程应用程序,应选择使用ArrayList。
2.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的代码,运行结果如下:
3. 本章总结
1. Map集合用于保存具有映射关系的数据,即以键值对(key->value)的方式来存储数据。因此在Map集合内部有两个集合,一个集合用于保存Map中的key(键),一个集合用于保存Map中的value(值),其中key和value可以是任意数据类型数据。
2. Set集合的特点。
3. Set集合的使用及迭代。
4. Map集合的迭代。