为了在程序中可以保存数目不确定的对象,Java提供了一系列特殊的类,这些类可以存储任意类型的对象,并且长度可变,这些类被统称为集合。集合类都位于java.util包中,使用时必须导包。
集合按照其存储结构可以分为两大类,单列集合Collection和双列集合Map,这两种集合的特点具体如下: ● Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是List和Set。其中,List的特点是元素有序、元素可重复。Set的特点是元素无序,而且不可重复。List接口的主要实现类有ArrayList和LinkedList,Set接口的主要实现类有HashSet和TreeSet。
● Map:双列集合类的根接口,用于存储具有键(Key)、值(Value)映射关系的元素,每个元素都包含一对键值,其中键值不可重复并且每个键最多只能映射到一个值,在使用Map集合时可以通过指定的Key找到对应的Value。例如,根据一个学生的学号就可以找到对应的学生。Map接口的主要实现类有HashMap和TreeMap。
整个集合类的继承体系如下图。
Collection是所有单列集合的父接口,它定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。Collection接口的常用如下表。
方法声明 | 功能描述 |
---|---|
boolean add(Object o) | 向集合中添加一个元素 |
boolean addAll(Collection c) | 将指定Collection中的所有元素添加到该集合中 |
void clear() | 删除该集合中的所有元素 |
boolean remove(Object o) | 删除该集合中指定的元素 |
boolean removeAll(Collection c) | 删除指定集合中的所有元素 |
boolean isEmpty() | 判断该集合是否为空 |
boolean contains(Object o) | 判断该集合中是否包含某个元素 |
boolean containsAll(Collection c) | 判断该集合中是否包含指定集合中的所有元素 |
Iterator iterator() | 返回在该集合的元素上进行迭代的迭代器(Iterator),用于遍历该集合所有元素 |
int size() | 获取该集合元素个数 |
List接口继承自Collection接口,是单列集合的一个重要分支。List集合允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引访问List集合中的指定元素。另外,List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引操作集合的特有方法。 List接口常用方法如下表。
方法声明 | 功能描述 |
---|---|
void add(int index,Object element) | 将元素element插入在List集合的index处 |
boolean addAll(int index,Collection c) | 将集合c所包含的所有元素插入到List集合的index处 |
Object get(int index) | 返回集合索引index处的元素 |
Object remove(int index) | 删除index索引处的元素 |
Object set(int index, Object element) | 将索引index处元素替换成element对象,并将替换后的元素返回 |
int indexOf(Object o) | 返回对象o在List集合中出现的位置索引 |
int lastIndexOf(Object o) | 返回对象o在List集合中最后一次出现的位置索引 |
List subList(int fromIndex, int toIndex) | 返回从索引fromIndex(包括)到 toIndex(不包括)处所有元素集合组成的子集合 |
ArrayList是List接口的一个实现类,它是程序中最常见的一种集合。在ArrayList内部封装了一个长度可变的数组对象,当存入的元素超过数组长度时,ArrayList会在内存中分配一个更大的数组来存储这些元素,因此可以将ArrayList集合看作一个长度可变的数组。
ArrayList集合中大部分方法都是从父类Collection和List继承过来的,其中add()方法和get()方法分别用于实现元素的存入和取出。接下来通过一个案例学习ArrayList集合的元素存取。
1 import java.util.*;
2 public class Example01 {
3 public static void main(String[] args) {
4 ArrayList list = new ArrayList(); // 创建ArrayList集5合
5 list.add("张三"); // 向集合中添加元素
6 list.add("李四");
7 list.add("王五");
8 list.add("赵六");
9 // 获取集合中元素的个数
10 System.out.println("集合的长度:" + list.size());
11 // 取出并打印指定位置的元素
12 System.out.println("第2个元素是:" + list.get(1));
13 }
14 }
上述代码中,第4行代码创建了一个list对象,第5~8行代码使用list对象调用add(Object o)方法向ArrayList集合中添加了4个元素,第10行代码使用list对象调用size()方法获取集合中元素个数并输出打印,第12行代码使用list对象调用ArrayList的get(int index)方法取出指定索引位置的元素并输出打印。从运行结果可以看出,索引位置为1的元素是集合中的第二个元素,这就说明集合和数组一样,索引的取值范围是从0开始的,最后一个索引是size-1,在访问元素时一定要注意索引不可超出此范围,否则会抛出角标越界异常IndexOutOfBoundsException。
由于ArrayList集合的底层是使用一个数组来保存元素,在增加或删除指定位置的元素时,会导致创建新的数组,效率比较低,因此不适合做大量的增删操作。因为这种数组的结构允许程序通过索引的方式来访问元素,所以使用ArrayList集合查找元素很便捷。
脚下留心:泛型安全机制问题
在IntelliJ IDEA中编译上述程序时,会得到警告信息,提示在使用ArrayList集合时并没有明确指定集合中存储什么类型的元素,会产生安全隐患,这涉及到泛型安全机制的问题。警告信息如下图。
在编写程序时,不要忘记使用“import java.util.ArrayList;”语句导包,否则IDEA会提示类型不能解决的错误信息,将鼠标移动到报出错误的ArrayList()上,错误显示如下图。
要解决此问题,只需单击图中错误显示小窗口中Import class的链接,这样IntelliJ IDEA就会自动导入ArrayList的包。在后面的案例中会大量地用到集合类,为了方便,程序中可以使用import java.util.;来进行导包,其中为通配符,整个语句的意思是将java.util包中的内容都导入进来。
ArrayList集合在查询元素时速度很快,但在增删元素时效率较低。为了克服这种局限性,可以使用List接口的另一个实现类LinkedList。LinkedList集合内部维护了一个双向循环链表,链表中的每一个元素都使用引用的方式来记住它的前一个元素和后一个元素,从而可以将所有的元素彼此连接起来。当插入一个新元素时,只需要修改元素之间的这种引用关系即可,删除一个节点也是如此。正因为这样的存储结构,所以LinkedList集合对于元素的增删操作具有很高的效率。
LinkedList集合添加元素和删除元素的过程如下图。
LinkedList集合定义了一些特有的方法如下表。
方法声明 | 功能描述 |
---|---|
void add(int index, E element) | 在此列表中指定的位置插入指定的元素 |
void addFirst(Object o) | 将指定元素插入此列表的开头 |
void addLast(Object o) | 将指定元素添加到此列表的结尾 |
Object getFirst() | 返回此列表的第一个元素 |
Object getLast() | 返回此列表的最后一个元素 |
Object removeFirst() | 移除并返回此列表的第一个元素 |
Object removeLast() | 移除并返回此列表的最后一个元素 |
接下来通过一个案例学习LinkedList方法的使用。
1 import java.util.*;
2 public class Example02 {
3 public static void main(String[] args) {
4 LinkedList link = new LinkedList(); // 创建LinkedList集合
5 link.add("张三");
6 link.add("李四");
7 link.add("王五");
8 link.add("赵六");
9 System.out.println(link.toString()); // 取出并打印该集合中的元素
10 link.add(3, "Student"); // 向该集合中指定位置插入元素
11 link.addFirst("First"); // 向该集合第一个位置插入元素
12 System.out.println(link);
13 System.out.println(link.getFirst()); // 取出该集合中第一个元素
14 link.remove(3); // 移除该集合中指定位置的元素
15 link.removeFirst(); // 移除该集合中第一个元素
16 System.out.println(link);
17 }
18 }
上述代码中,第4行代码是创建了一个LinkedList集合,第5~8行代码是在LinkedList集合中存入4个元素;第10~11行代码是通过add(int index,Object o)和addFirst(Object o)方法分别在集合的指定位置和第一个位置(索引0位置)插入元素;第14~15行代码是使用remove(int index)和removeFirst()方法将指定位置和集合中的第一个元素移除。这样就完成了元素的增删操作。由此可见,使用LinkedList对元素进行增删操作是非常便捷的。
在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,Java专门提供了一个接口Iterator。Iterator接口也是集合中的一员,但它与Collection、Map接口有所不同。Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器。
接下来通过一个案例学习如何使用Iterator迭代集合中的元素。
1 import java.util.*;
2 public class Example03 {
3 public static void main(String[] args) {
4 ArrayList list = new ArrayList(); // 创建ArrayList集合
5 list.add("张三"); // 向该集合中添加字符串
6 list.add("李四");
7 list.add("王五");
8 list.add("赵六");
9 Iterator it = list.iterator(); // 获取Iterator对象
10 while (it.hasNext()) { // 判断ArrayList集合中是否存在下一个元素
11 Object obj = it.next(); // 取出ArrayList集合中的元素
12 System.out.println(obj);
13 }
14 }
15 }
上述代码中,第9行代码定义了一个迭代器。当遍历元素时,首先通过调用ArrayList集合的iterator()方法获得迭代器对象;第10~13行代码是遍历ArrayList集合,首先使用hasNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。需要注意的是,在通过next()方法获取元素时,必须保证要获取的元素存在,否则,会抛出NoSuchElementException异常。
Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了让初学者能更好地理解迭代器的工作原理,接下来通过一个图例演示Iterator对象迭代元素的过程。
上图中,在调用Iterator的next()方法之前,迭代器的索引位于第一个元素之前,不指向任何元素,当第一次调用迭代器的next()方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next()方法时,迭代器的索引会指向第二个元素并将该元素返回,以此类推,直到hasNext()方法返回false,表示到达了集合的末尾,终止对元素的遍历。
通过迭代器获取ArrayList集合中的元素时,这些元素的类型都是Object类型,如果想获取到特定类型的元素,则需要进行对数据类型强制转换。
脚下留心:并发修改异常
在使用Iterator迭代器对集合中的元素进行迭代时,如果调用了集合对象的remove()方法去删除元素之后,继续使用迭代器遍历元素,会出现异常。接下来通过一个案例演示这种异常。假设在一个集合中存储了学校所有学生的姓名,由于一个名为Annie的学生中途转学,这时就需要在迭代集合时找出该元素并将其删除,具体代码如下。
1 import java.util.*;
2 public class Example04 {
3 public static void main(String[] args) {
4 ArrayList list = new ArrayList(); //创建ArrayList集合
5 list.add("张三");
6 list.add("李四");
7 list.add("王五");
8 Iterator it = list.iterator(); // 获得Iterator对象
9 while (it.hasNext()) { // 判断该集合是否有下一个元素
10 Object obj = it.next(); // 获取该集合中的元素
11 if ("张三".equals(obj)) { // 判断该集合中的元素是否为张三
12 list.remove(obj); // 删除该集合中的元素
13 }
14 }
15 System.out.println(list);
16 }
17 }
上述程序在运行时出现了并发修改异常ConcurrentModificationException。这个异常是迭代器对象抛出的,出现异常的原因是集合在迭代器运行期间删除了元素,会导致迭代器预期的迭代次数发生改变,导致迭代器的结果不准确。
要解决上述问题,可以采用两种方式,下面分别介绍。 第一种方式:从业务逻辑上讲只想将姓名为Annie的学生删除,至于后面还有多少学生并不需要关心,只需找到该学生后跳出循环不再迭代即可,也就是在第12行代码下面增加一个break语句,代码如下:
if ("张三".equals(obj)) {
list.remove(obj);
break;
}
第二种方式:如果需要在集合的迭代期间对集合中的元素进行删除,可以使用迭代器本身的删除方法,将第12行代码替换成it.remove()即可解决这个问题:
if ("张三".equals(obj)) {
it.remove();
}
替换代码后再次运行程序,运行结果如下图。
由上图运行结果可知,学员Annie确实被删除了,并且没有出现异常。因此可以得出结论,调用迭代器对象的remove()方法删除元素所导致的迭代次数变化,对于迭代器对象本身来讲是可预知的。
虽然Iterator可以用来遍历集合中的元素,但写法上比较繁琐,为了简化书写,从JDK5开始,提供了foreach循环。foreach循环是一种更加简洁的for循环,也称增强for循环。foreach循环用于遍历数组或集合中的元素,具体语法格式如下: for(容器中元素类型 临时变量 :容器变量) { 执行语句 }
从上面的格式可以看出,与for循环相比,foreach循环不需要获得容器的长度,也不需要根据索引访问容器中的元素,但它会自动遍历容器中的每个元素。接下来通过一个案例演示foreach循环的用法。
1 import java.util.*;
2 public class Example05 {
3 public static void main(String[] args) {
4 ArrayList list = new ArrayList();// 创建ArrayList集合
5 list.add("aaa"); // 向ArrayList集合中添加字符串元素
6 list.add("bbb");
7 list.add("ccc");
8 for (Object obj : list) { // 使用foreach循环遍历ArrayList对象
9 System.out.println(obj); // 取出并打印ArrayList集合中的元素
10 }
11 }
12 }
上述代码中,第4~7行代码是声明了一个ArrayList集合并向集合中添加了3个元素。第8行代码是使用foreach循环遍历ArrayList集合并打印。可以看出,foreach循环在遍历集合时语法非常简洁,没有循环条件,也没有迭代语句,所有这些工作都交给虚拟机去执行了。foreach循环的次数是由容器中元素的个数决定的,每次循环时,foreach中都通过变量将当前循环的元素记住,从而将集合中的元素分别打印出来。
脚下留心: foreach循环缺陷
foreach循环虽然书写起来很简洁,但在使用时也存在一定的局限性。当使用foreach循环遍历集合和数组时,只能访问集合中的元素,不能对其中的元素进行修改。接下来以一个String类型的数组为例演示foreach循环的缺陷。
1 public class Example06 {
2 static String[] strs = { "aaa", "bbb", "ccc" };
3 public static void main(String[] args) {
4 // foreach循环遍历数组
5 for (String str : strs) {
6 str = "ddd";
7 }
8 System.out.println("foreach循环修改后的数组:" + strs[0] + "," +
9 strs[1] + ","+ strs[2]);
10 // for循环遍历数组
11 for (int i = 0; i < strs.length; i++) {
12 strs[i] = "ddd";
13 }
14 System.out.println("普通for循环修改后的数组:" + strs[0] + "," +
15 strs[1] + ","+ strs[2]);
16 }
17 }
上述代码中,分别使用foreach循环和普通for循环去修改数组中的元素。从运行结果可以看出foreach循环并不能修改数组中元素的值。原因是第6行代码中的str = "ddd"只是将临时变量str指向了一个新的字符串,这和数组中的元素没有一点关系。而在普通for循环中,是可以通过索引的方式来引用数组中的元素并将其值进行修改的。
像商城和超市这样的地方,都需要有自己的库房,并且库房商品的库存变化有专人记录,这样才能保证商城和超市正常运转。 本例要求编写一个程序,模拟库存管理系统。该系统主要包括系统首页、商品入库、商品显示和删除商品功能。每个功能的具体要求如下: 系统的首页:用于显示系统所有的操作,并且可以选择使用某一个功能。
商品入库功能:首先提示是否要录入商品,根据用户输入的信息判断是否需要录入商品。如果需要录入商品,则需要用户输入商品的名称、颜色、价格和数量等信息。录入完成后,提示商品录入成功并打印所有商品。如果不需要录入商品,则返回系统首页。 商品显示功能:用户选择商品显示功能后,在控制台打印仓库所有商品信息。
删除商品功能:用户选择删除商品功能后,根据用户输入的商品编号删除商品,并在控制台打印删除后的所有商品。 本案例要求使用Collection集合存储自定义的对象,并用迭代器、增强for循环遍历集合。
(1)定义仓库产品类,自定义对象定义属性。
(2)在类中定义集合,将要存储的自定义对象作为泛型。
(3)主方法中为集合添加初始化的数据,采用无线循环的方式显示菜单栏。用if...else语句判断用户索要进行的操作,用户可以键盘输入1、2、3来选择对应的操作。如果输入1可以进行商品入库,输入2可以显示库房内所有商品,输入3可以删除进行商品出库,如果不是1、2、3则提示用户操作失败。
(4) 定义商品入库的方法,选择后首先提示用户是否录入商品。输入“no”不录入商品。直接退出并显示所有商品。输入其他任何数据开始录入商品并提示键盘输入什么。录入完毕后退出并显示所有商品。
(5) 定义显示仓库所有商品的方法,用Iterator迭代器的方法循环遍历集合打印,实现商品显示。
(6) 定义商品出库的方法,用集合删除的方法移除要删除的商品。
1 public class Phone {
2 private String name;
3 private String color;
4 private double price;
5 private int num;
6 }
1 package com.itheima;
2 import java.util.ArrayList;
3 import java.util.Collection;
4 import java.util.Iterator;
5 import java.util.List;
6 import java.util.Scanner;
7 public class example1 {
8 static Collection c = new ArrayList();
9 public static void main(String[] args) {
10 c.add(new Phone("小米9", "玫瑰金",3999.00,15));
11 c.add(new Phone("小米9", "幻彩紫", 3699, 28));
12 c.add(new Phone("华为P30", "白金色", 5699, 2));
13 c.add(new Phone("华为P30", "绚彩白", 5999, 18));
14 c.add(new Phone("VIVO PLus9", "幻彩紫", 2699, 28));
15 c.add(new Phone("魅族 16th", "紫金黑", 5229, 10));
16 c.add(new Phone("苹果 11", "土豪金", 8999, 51));
17 while(true) {
18 System.out.println("欢迎使用库房管理系统,请选择要进行的操作");
19 System.out.println("1. 商品入库");
20 System.out.println("2. 商品显示");
21 System.out.println("3. 删除商品");
22 Scanner sc = new Scanner(System.in);
23 int s = sc.nextInt();
24 if(s == 1) {
25 addwarehouse();
26 System.out.println("商品入库成功,入库后仓库商品如下:");
27 warehouse();
28 }else if(s==2) {
29 warehouse();
30 }else if(s ==3) {
31 System.out.println("请输入你要删除的商品编号:");
32 int index=sc.nextInt();
33 delwarehouse(index);
34 System.out.println("商品出库成功,出库后仓库商品如下:");
35 warehouse();
36 }else {
37 System.out.println("操作失败!!!");
38 }
39 }
40 }
41 private static void addwarehouse() {
42 while(true) {
43 System.out.println("您是否录入商品?");
44 Scanner w = new Scanner(System.in);
45 String s=w.next();
46 if(!s.equals("no")) {
47 Scanner sc1 = new Scanner(System.in);
48 System.out.println("请输入商品的名称:");
49 String name=sc1.next();
50 System.out.println("请输入商品的颜色:");
51 String color=sc1.next();
52 System.out.println("请输入商品的价格:");
53 int p=sc1.nextInt();
54 System.out.println("请输入商品的数量:");
55 int n=sc1.nextInt();
56 c.add(new Phone(name,color,p,n));
57 break;
58 }else {
59 break;
60 }
61 }
62 }
63 private static void warehouse() {
64 //获取迭代器
65 Iterator it = c.iterator();
66 while(it.hasNext()) {
67 Phone s = (Phone)it.next(); //向下转型
68 System.out.println(s.getName()+ "..." + s.getColor() +
69 "..." + s.getPrice()+ "..." + s.getNum());
70 }
71 }
72 private static void delwarehouse(int index) {
73 c.remove(index);
74 }
}
在一所学校中,对学生人员流动的管理是很麻烦的,本案例要求编写一个学生管理系统,实现对学生信息的添加、删除、修改和查询功能。每个功能的具体要求如下: 系统的首页:用于显示系统所有的操作,并根据用户在控制台的输入选择需要使用的功能。 查询功能:用户选择该功能后,在控制台打印所有学生的信息。
添加功能:用户选择该功能后,要求用户在控制台输入学生学号、姓名、年龄和居住地的基本信息。在输入学号时,判断学号是否被占用,如果被占用则添加失败,并给出相应提示;反之则提示添加成功。 删除功能:用户选择该功能后,提示用户在控制台输入需要删除学生的学号,如果用户输入的学号存在则提示删除成功,反之则提示删除失败。
修改功能:用户选择该功能后,提示用户在控制台输入需要修改的学生学号、姓名、年龄和居住地学生信息,并使用输入的学生学号判断是否有此人,如果有则修改原有的学生信息,反正则提示需要修改的学生信息不存在。 退出功能:用户选择该功能后,程序正常关闭。 本案例要求使用List集合存储自定义的对象,使用List集合的中常用方法实现相关的操作。
(1)定义学生类,自定义对象属性。
(2)学生管理系统的主界面的代码编写,创建集合对象,用于存储学生数据,打印学生管理系统主界面的相关功能,创建键盘输入功能,用switch语句实现选择的功能。最后为了实现多次操作并且手动退出系统。用while(true)循环实现.
(3) 学生管理系统查询所有学生信息的代码编写,首先判断集合中是否有数据,如果没有数据就给出提示,并让该方法不在继续往下执行。如果有数据。遍历集合输出打印数据。
(4) 学生管理系统添加学生信息的代码编写,首先输入学生学号,判断学号有没有被人占用,如果被占用重新输入学号,没有被占用继续录入学生姓名、年龄。家庭住址等信息。创建学生对象,将录入的数据存入对象。最后将学生对象添加入集合,添加学生信息成功。
(5) 学生管理系统删除学生信息的代码编写,键盘录入一个学号,到集合中去查找,看是否有学生使用该学号,如果有就删除该学生信息。如果没有学生使用该学号,返回主界面。
(6)学生管理系统修改学生信息的代码编写。键盘录入一个学号,到集合中去查找,看是否有学生使用该学号,如果过有就修改学生信息。反之返回主界面。
1 public class Student {
2 //学号
3 private String id;
4 //姓名
5 private String name;
6 //年龄
7 private String age;
8 //居住地
9 private String address;
10 }
1 package com.itheima;
2 import java.util.ArrayList;
3 import java.util.Scanner;
4 public class example2 {
5 public static void main(String[] args) {
6 //创建集合对象,用于存储学生数据
7 ArrayList array = new ArrayList();
8 //为了让程序能够回到这里来,我们使用循环
9 while(true) {
10 //这是学生管理系统的主界面
11 System.out.println("--------欢迎来到学生管理系统--------");
12 System.out.println("1 查看所有学生");
13 System.out.println("2 添加学生");
14 System.out.println("3 删除学生");
15 System.out.println("4 修改学生");
16 System.out.println("5 退出");
17 System.out.println("请输入你的选择:");
18 //创建键盘录入对象
19 Scanner sc = new Scanner(System.in);
20 String choiceString = sc.nextLine();
21 //用switch语句实现选择
22 switch(choiceString) {
23 case "1":
24 //查看所有学生
25 findAllStudent(array);
26 break;
27 case "2":
28 //添加学生
29 addStudent(array);
30 break;
31 case "3":
32 //删除学生
33 deleteStudent(array);
34 break;
35 case "4":
36 //修改学生
37 updateStudent(array);
38 break;
39 case "5":
40 //退出
41 //System.out.println("谢谢你的使用");
42 //break;
43 default:
44 System.out.println("谢谢你的使用");
45 System.exit(0); //JVM退出
46 break;
47 }
48 }
49 }
50 //查看所有学生
51 public static void findAllStudent(ArrayList array) {
52 //首先来判断集合中是否有数据,如果没有数据,就给出提示,并让该方法不继续往
53 下执行
54 if(array.size() == 0) {
55 System.out.println("不好意思,目前没有学生信息可供查询,请回去重新选择你
56 的操作");
57 return;
58 }
59 //\t 其实就是一个tab键的位置
60 System.out.println("学号\t姓名\t年龄\t居住地");
61 for(int x=0; x array) {
70 //创建键盘录入对象
71 Scanner sc = new Scanner(System.in);
72 //为了让id能够被访问到,我们就把id定义在了循环的外面
73 String id;
74 //为了让代码能够回到这里,用循环
75 while(true) {
76 System.out.println("请输入学生学号:");
77 //String id = sc.nextLine();
78 id = sc.nextLine();
79 //判断学号有没有被人占用
80 //定义标记
81 boolean flag = false;
82 //遍历集合,得到每一个学生
83 for(int x=0; x array) {
116 //删除学生的思路:键盘录入一个学号,到集合中去查找,看是否有学生使用的是该
117 学号,如果有就删除该学生
118 //创建键盘录入对象
119 Scanner sc = new Scanner(System.in);
120 System.out.println("请输入你要删除的学生的学号:");
121 String id = sc.nextLine();
122 //我们必须给出学号不存在的时候的提示
123 //定义一个索引
124 int index = -1;
125 //遍历集合
126 for(int x=0; x array) {
145 //修改学生的思路:键盘录入一个学号,到集合中去查找,看是否有学生使用的是该
146 学号,如果有就修改该学生
147 //创建键盘录入对象
148 Scanner sc = new Scanner(System.in);
149 System.out.println("请输入你要修改的学生的学号:");
150 String id = sc.nextLine();
151 //定义一个索引
152 int index = -1;
153 //遍历集合
154 for(int x=0; x
Set接口和List接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。 Set接口主要有两个实现类,分别是HashSet和TreeSet。其中,HashSet是根据对象的哈希值来确定元素在集合中的存储位置,具有良好的存取和查找性能。TreeSet则是以二叉树的方式来存储元素,它可以实现对集合中的元素进行排序。
HashSet是Set接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的。接下来通过一个案例演示HashSet集合的用法。
1 import java.util.*;
2 public class Example07 {
3 public static void main(String[] args) {
4 HashSet set = new HashSet(); // 创建HashSet集合
5 set.add("张三"); // 向该Set集合中添加字符串
6 set.add("李四");
7 set.add("王五");
8 set.add("李四"); // 向该Set集合中添加重复元素
9 Iterator it = set.iterator(); // 获取Iterator对象
10 while (it.hasNext()) { // 通过while循环,判断集合中是否有元素
11 Object obj = it.next();// 如果有元素,就通过迭代器的next()方法获取元素
12 System.out.println(obj);
13 }
14 }
15 }
上述代码中,第4~8行代码是声明了一个HashSet集合并通过add()方法向HashSet集合依次添加了四个字符串;第9行代码是声明了一个迭代器对象it,第10~13行代码是通过Iterator迭代器遍历所有的元素并输出。从打印结果可以看出,取出元素的顺序与添加元素的顺序并不一致,并且重复存入的字符串对象“李四”被去除了,只添加了一次。
HashSet集合之所以能确保不出现重复的元素,是因为它在存入元素时做了很多工作。当调用HashSet集合的add()方法存入元素时,首先调用当前存入对象的hashCode()方法获得对象的哈希值,然后根据对象的哈希值计算出一个存储位置。如果该位置上没有元素,则直接将元素存入,如果该位置上有元素存在,则会调用equals()方法让当前存入的元素依次和该位置上的元素进行比较,如果返回的结果为false就将该元素存入集合,返回的结果为true则说明有重复元素,就将该元素舍弃。
HashSet存储元素的流程如下图。
接下来通过一个案例演示向HashSet存储字符串。
1 import java.util.*;
2 class Student {
3 String id;
4 String name;
5 public Student(String id,String name) { // 创建构造方法
6 this.id=id;
7 this.name = name;
8 }
9 public String toString() { // 重写toString()方法
10 return id+":"+name;
11 }
12 }
13 public class Example08 {
14 public static void main(String[] args) {
15 HashSet hs = new HashSet(); // 创建HashSet集合
16 Student stu1 = new Student("1", "张三"); // 创建Student对象
17 Student stu2 = new Student("2", "李四");
18 Student stu3 = new Student("2", "李四");
19 hs.add(stu1);
20 hs.add(stu2);
21 hs.add(stu3);
22 System.out.println(hs);
23 }
24 }
上述代码中,第15行代码声明了一个HashSet集合,第16~18行代码分别声明了3个Student对象,第19~22行代码是分别将3个Student对象存入HashSet集合中并输出。图6-18所示的运行结果中出现了两个相同的学生信息“2:李四”,这样的学生信息应该被视为重复元素,不允许同时出现在HashSet集合中。之所以没有去掉这样的重复元素,是因为在定义Student类时没有重写hashCode()和equals()方法。
接下来改写上述案例,存储id编号相同的学生。
1 import java.util.*;
2 class Student {
3 private String id;
4 private String name;
5 public Student(String id, String name) {
6 this.id = id;
7 this.name = name;
8 }
9 // 重写toString()方法
10 public String toString() {
11 return id + ":" + name;
12 }
13 // 重写hashCode方法
14 public int hashCode() {
15 return id.hashCode(); // 返回id属性的哈希值
16 }
17 // 重写equals方法
18 public boolean equals(Object obj) {
19 if (this == obj) { // 判断是否是同一个对象
20 return true; // 如果是,直接返回true
21 }
22 if (!(obj instanceof Student)) { // 判断对象是为Student类型
23 return false;
24 }
25 Student stu = (Student) obj; // 将对象强转为Student类型
26 boolean b = this.id.equals(stu.id); // 判断id值是否相同
27 return b; // 返回判断结果
28 }
29 }
30 public class Example09 {
31 public static void main(String[] args) {
32 HashSet hs = new HashSet(); // 创建HashSet对象
33 Student stu1 = new Student("1", "张三"); // 创建Student对象
34 Student stu2 = new Student("2", "李四");
35 Student stu3 = new Student("2", "李四");
36 hs.add(stu1); // 向集合存入对象
37 hs.add(stu2);
38 hs.add(stu3);
39 System.out.println(hs); // 打印集合中的元素
40 }
41 }
在上述代码中,Student类重写了Object类的hashCode()和equals()方法。在hashCode()方法中返回id属性的哈希值,在equals()方法中比较对象的id属性是否相等,并返回结果。当调用HashSet集合的add()方法添加stu3对象时,发现它的哈希值与stu2对象相同,而且stu2.equals(stu3)返回true,HashSet集合认为两个对象相同,因此重复的Student对象被成功去除了。
HashSet集合存储的元素是无序的,如果想让元素的存取顺序一致,可以使用Java中提供的LinkedHashSet类,LinkedHashSet类是HashSet的子类,与LinkedList一样,它也使用双向链表来维护内部元素的关系。 接下来通过一个案例学习LinkedHashSet类的用法。
1 import java.util.Iterator;
2 import java.util.LinkedHashSet;
3 public class Example10 {
4 public static void main(String[] args) {
5 LinkedHashSet set = new LinkedHashSet();
6 set.add("张三"); // 向该Set集合中添加字符串
7 set.add("李四");
8 set.add("王五");
9 Iterator it = set.iterator(); // 获取Iterator对象
10 while (it.hasNext()) { //通过while循环,判断集合中是否有元素
11 Object obj = it.next();
12 System.out.println(obj);
13 }
14 }
15 }
HashSet集合存储的元素是无序的和不可重复的,为了对集合中的元素进行排序,Set接口提供了另一个可以对HashSet集合中元素进行排序的类——TreeSet。
接下来通过一个案例演示TreeSet集合的用法。
1 import java.util.TreeSet;
2 public class Example11 {
3 public static void main(String[] args) {
4 TreeSet ts = new TreeSet();
5 ts.add(3);
6 ts.add(1);
7 ts.add(1);
8 ts.add(2);
9 ts.add(3);
10 System.out.println(ts);
11 }
12 }
上述代码中,第4行代码是声明了一个TreeSet集合,第5~10行代码是通过add()方法向TreeSet集合依次添加了五个整数类型的元素,然后将该集合打印输出。从打印结果可以看出,添加的元素已经自动排序,并且重复存入的整数1和3只添加了一次。
TreeSet集合之所以可以对添加的元素进行排序,是因为元素的类可以实现Comparable接口 (基本类型的包装类,String类都实现了该接口),Comparable接口强行对实现它的每个类的对象进行整体排序,这种排序被称为类的自然排序。Comparable接口的 compareTo() 方法被称为自然比较方法。如果将自定义的Student对象存入TreeSet,TreeSet将不会对添加的元素进行排序,Student对象必须实现Comparable接口并重写compareTo()方法实现对象元素的顺序存取。
接下来通过一个案例讲解使用compareTo()方法实现对象元素的顺序存取。
1 import java.util.TreeSet;
2 class Student implements Comparable {
3 private String id;
4 private String name;
5 public Student(String id, String name) {
6 this.id = id;
7 this.name = name;
8 }
9 // 重写toString()方法
10 public String toString() {
11 return id + ":" + name;
12 }
13 @Override
14 public int compareTo(Student o) {
15 // return 0; //集合中只有一个元素
16 // return 1; //集合按照怎么存就怎么取
17 return -1; //集合按照存入顺序倒过来进行存储
18 }
19 }
20 public class Example12 {
21 public static void main(String[] args) {
22 TreeSet ts = new TreeSet();
23 ts.add(new Student("1","张三"));
24 ts.add(new Student("2", "李四"));
25 ts.add(new Student("2", "王五"));
26 System.out.println(ts);
27 }
28 }
上述代码中,第2行代码是定义了一个Student类并继承第22~26行代码是向TreeSet集合存入三个Student对象,并将这三个对象迭代输出。从图6-22所示的运行结果可以看出,TreeSet按照存入元素的顺序倒过来存入了集合中,因为Student类实现了Comparable接口,并重写了compareTo()方法,当compareTo()方法返回0的时候集合中只有一个元素;当compareTo()方法返回正数的时候集合会正常存取;当compareTo()方法返回负数的时候集合会倒序存储。由于篇幅有限,这里我们只演示compareTo()方法返回负数的情况,其他两种读者可自己运行观察效果。
上面介绍了TreeSet集合的自然排序,它还有另一种实现排序的方式,即实现Comparator接口,重写compare()方法和equals()方法,但是由于所有的类默认继承Object,而Object中有equals()方法,所以自定义比较器类时,不用重写equals()方法,只需要重写compare()方法,这种排序被称为比较器排序。
接下来通过一个案例学习将自定义的Student对象通过比较器的方式存入TreeSet集合。
1 import java.util.Comparator;
2 import java.util.TreeSet;
3 class Student{
4 private String id;
5 private String name;
6 public Student(String id, String name) {
7 this.id = id;
8 this.name = name;
9 }
10 // 重写toString()方法
11 public String toString() {
12 return id + ":" + name;
13 }
14 }
15 public class Example13 {
16 public static void main(String[] args) {
17 TreeSet ts = new TreeSet(new Comparator() {
18 @Override
19 public int compare(Object o1, Object o2) {
20 return -1;
21 }
22 });
23 ts.add(new Student("1","张三"));
24 ts.add(new Student("2", "李四"));
25 ts.add(new Student("2", "王五"));
26 System.out.println(ts);
27 }
28 }
上述代码中,第17~22行代码是声明了一个TreeSet集合并通过匿名内部类的方式实现了Comparator接口,然后重写了compare()方法。
互联网为我们提供了巨大的便利,如微信带给我们的视频、淘宝带给我们便利的购物等,但这些APP都需要有一个账户才可以登录,而账户需要注册可以获取。
本例要求编写一个程序,模拟用户注册。用户输入用户名、密码、确认密码、生日(格式为yyyy--mm—dd为正确)、手机号(手机号长度为11位,并且以13、15、17、18为开头的手机号为正确)、邮箱(包含符号“@”为正确)信息之后,判断信息正确后,验证用户是否重复,重复则给出相应提示,如果不重复则注册成功。案例要求使用HashSet集合实现。
(1) 为了便于存储用户的信息。需要创建一个用户类,在类中重写其中的HashCode()方法,令其返回用户的额哈希值,再重写equals()方法,来比较对象的用户属性是否相等。
(2) 创建一个用户注册类来模拟用户注册信息,该类中可以用HashSet集合创建一个数据列表,然后向列表中添加两条初始用户信息。
(3) 从控制台获取用户填写信息,通过Scanner类的nextline()方法实现,获取后,需要将获取的数据进行校验。
(4) 单独创建一个校验类,在该类中实现校验用户输入信息的方法。校验结束后,如果效验结果错误就直接返回错误信息,这里可以分别声明一个校验结果的变量和一个校验状态的变量。校验结果变量用于存储提示信息,校验状态变量用于存储结果的判断标识。
(5) 当用户输入的信息不满足规定的格式时,需要修改变量的状态并且存储错误信息。
(6) 判断校验状态,如果所有信息都通过校验,则将用户信息创建为用户对象,通过将对象添加到用户列表返回结果来判断用户是否重复,并记录下返回结果信息。
1 package com.itheima.shiyan6_3;
2 import java.util.Date;
3 //用户信息
4 public class User {
5 private String userName; // 用户名
6 private String password; // 密码
7 private Date birthday; // 生日
8 private String telNumber; // 手机号码
9 private String email; // 邮箱
10 public User() {
11 }
12 public User(String userName, String password, Date birthday,
13 String telNumber, String email) {
14 this.userName = userName;
15 this.password = password;
16 this.birthday = birthday;
17 this.telNumber = telNumber;
18 this.email = email;
19 }
20 // 重写hashCode与equals方法
21 @Override
22 public int hashCode() {// 重写hashCode方法,以用户名作为是否重复的依据
23 return userName.hashCode();
24 }
25 @Override
26 public boolean equals(Object obj) {
27 if (this == obj) {// 判断是否是同一个对象
28 return true;// 如果是同一个对象,直接返回true
29 }
30 if (obj == null) {// 判断这个对象是否为空
31 return false;// 如果对象是空的,直接返回false
32 }
33 if (getClass() != obj.getClass()) {// 判断这个对象是否是User类型
34 return false;// 如果不是,直接返回false
35 }
36 User other = (User) obj;// 将对象强转为User类型
37 if (userName == null) {// 判断集合中用户名是否为空
38 if (other.userName != null) {// 判断对象中的用户名是否为空
39 // 如果集合中用户名为空并且对象中用户名不为空,则返回false
40 return false;
41 }
42 // 判断用户名是否相同
43 } else if (!userName.equals(other.userName)) {
44 return false;// 如果不同,返回false
45 }
46 return true;
47 }
48 }
1package com.itheima.shiyan6_3;
2import java.util.Date;
3import java.util.HashSet;
4import java.util.Scanner;
5public class UserRegister {
6public static HashSet USER_DATA = new HashSet(); // 用户数据
7 public static void main(String[] args) {
8 initData();// 初始化人员信息
9 Scanner scan = new Scanner(System.in);
10 System.out.print("请输入用户名:");
11 String userName = scan.nextLine();// 获取用户名
12 System.out.print("请输入密码:");
13 String password = scan.nextLine();// 获取密码
14 System.out.print("请重复密码:");
15 String repassword = scan.nextLine();// 获取重复密码
16 System.out.print("出生日期:");
17 String birthday = scan.nextLine();// 获取出生日期
18 System.out.print("手机号码:");
19 String telNumber = scan.nextLine();// 获取手机号码
20 System.out.print("电子邮箱:");
21 String email = scan.nextLine();// 获取电子邮箱
22 // 校验用户信息,返回登录状态信息
23 CheckInfo checkInfo = new CheckInfo(USER_DATA);
24 String result = checkInfo.checkAction(userName, password,
25 repassword, birthday, telNumber, email);
26 System.out.println("注册结果:" + result);
27 }
28 // 初始化数据,创建两个已存在的用户信息
29 private static void initData() {
30 User user = new User("迪丽热巴", "zz,123", new Date(),
31 "18810319240", "[email protected]");
32 User user2 = new User("吴宣仪", "zq,123", new Date(),
33 "18618121193", "[email protected]");
34 USER_DATA.add(user);
35 USER_DATA.add(user2);
36 }
37}
1 package com.itheima.shiyan6_3;
2 import java.text.DateFormat;
3 import java.text.ParseException;
4 import java.text.SimpleDateFormat;
5 import java.util.Date;
6 import java.util.HashSet;
7 public class CheckInfo {
8 public static HashSet USER_DATA = new HashSet(); // 用户数据
9 public CheckInfo(HashSet USER_DATA) {
10 this.USER_DATA = USER_DATA;
11 }
12 // 校验用户信息,返回登录状态信息
13 public String checkAction(String userName, String password,
14 String rePassword,String birthday, String phone, String email) {
15 StringBuilder result = new StringBuilder();
16 // 1代表成功 2代表失败
17 int state = 1;
18 // 密码判断
19 if (!password.equals(rePassword)) {// 判断密码和重复密码是否相同
20 result.append("两次输入密码不一致!\r\n");
21 state = 2;
22 }
23 // 生日判断
24 if (birthday.length() != 10) {// 字符串长度不为10,则认为格式错误
25 result.append("生日格式不正确!\r\n");
26 state = 2;
27 } else {
28 for (int i = 0; i < birthday.length(); i++) {
29 Character thisChar = birthday.charAt(i);
30 if (i == 4 || i == 7) {
31 if (!(thisChar == '-')) {// 验证第4位和第7位是否是
32 符号“-”
33 result.append("生日格式不正确!\r\n");
34 state = 2;
35 }
36 } else {// 验证除了第4位和第7位的字符是否是0~9的数字
37 if (!(Character.isDigit(thisChar))) {
38 result.append("生日格式不正确!\r\n");
39 state = 2;
40 }
41 }
42 }
43 }
44 // 手机号判断
45 // 判断手机号长度不等于11位则认为此手机号无效
46 if (phone.length() != 11) {
47 result.append("手机号码不正确!\r\n");
48 state = 2;
49 // 默认有效手机号为13、15、17和18开头的手机号
50 } else if (!(phone.startsWith("13") || phone.startsWith("15")
51 || phone.startsWith("17") || phone.startsWith("18"))){
52 result.append("手机号码不正确!\r\n");
53 state = 2;
54 }
55 // 邮箱判断
56 // 判断邮箱地址,默认不带@符号的邮箱为无效邮箱
57 if (!email.contains("@")) {
58 result.append("邮箱不正确!\r\n");
59 state = 2;
60 }
61 // 如果以上信息校验无误,则将新用户加入到集合
62 if (state == 1) {
63 // 格式化日期返回Date对象
64 //定义日期格式
65 DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
66 Date dateBirthday = null;
67 try {
68 // 将生日格式化成日期格式
69 dateBirthday = format.parse(birthday);
70 } catch (ParseException e) {
71 e.printStackTrace();
72 }
73 User newUser = new User(userName, rePassword,
74 dateBirthday, phone, email);
75 // 将用户添加到列表中,同时可根据HashSet判断出用户名有没有重复
76 if (!USER_DATA.add(newUser)) {
77 result.append("用户重复!");
78 state = 2;
79 }
80 if (state == 1) {
81 result.append("注册成功!");
82 }
83 }
84 return result.toString();
85 }
86 }
Map接口是一种双列集合,它的每个元素都包含一个键对象Key和值对象Value,键和值对象之间存在一种对应关系,称为映射。从Map集合中访问元素时,只要指定了Key,就能找到对应的Value。
Map接口常用方法如下表。
方法声明 | 功能描述 |
---|---|
void put(Object key, Object value) | 将指定的值与此映射中的指定键关联(可选操作) |
Object get(Object key) | 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回null |
void clear() | 移除所有的键值对元素 |
V remove(Object key) | 根据键删除对应的值,返回被删除的值 |
int size() | 返回集合中的键值对的个数 |
boolean containsKey(Object key) | 如果此映射包含指定键的映射关系,则返回 true。 |
boolean containsValue(Object value) | 如果此映射将一个或多个键映射到指定值,则返回 true |
Set keySet() | 返回此映射中包含的键的 Set 视图 |
Collection |
返回此映射中包含的值的 Collection 视图 |
Set |
返回此映射中包含的映射关系的Set视图 |
HashMap集合是Map接口的一个实现类,用于存储键值映射关系,但HashMap集合没有重复的键并且键值无序。接下来通过一个案例学习HashMap的用法。
1 import java.util.*;
2 public class Example14 {
3 public static void main(String[] args) {
4 HashMap map = new HashMap(); // 创建Map对象
5 map.put("1", "张三"); // 存储键和值
6 map.put("2", "李四");
7 map.put("3", "王五");
8 System.out.println("1:" + map.get("1")); // 根据键获取值
9 System.out.println("2:" + map.get("2"));
10 System.out.println("3:" + map.get("3"));
11 }
12 }
上述代码中,第4~7行代码声明了一个HashMap集合并通过Map的put(Object key,Object value)方法向集合中加入3个元素,第8~10行代码是通过Map的get(Object key)方法获取与键对应的值。
Map集合中的键具有唯一性,现在向Map集合中存储一个相同的键看看会出现什么情况,在第7行代码下面增加一行代码,如下所示: map.put("3", "赵六"); 修改之后,再次运行程序,运行结果如下图。
Map中仍然只有3个元素,只是第二次添加的值“赵六”覆盖了原来的值“王五”,这也证实了Map中的键必须是唯一的,不能重复,如果存储了相同的键,后存储的值则会覆盖原有的值,简而言之就是:键相同,值覆盖。 在程序开发中,经常需要取出Map中所有的键和值,那么如何遍历Map中所有的键值对呢?有两种方式可以实现,第一种方式就是先遍历Map集合中所有的键,再根据键获取相应的值。
接下来通过一个案例来演示先遍历Map集合中所有的键,再根据键获取相应的值。
1 import java.util.*;
2 public class Example15 {
3 public static void main(String[] args) {
4 HashMap map = new HashMap(); // 创建Map集合
5 map.put("1", "张三"); // 存储键和值
6 map.put("2", "李四");
7 map.put("3", "王五");
8 Set keySet = map.keySet(); // 获取键的集合
9 Iterator it = keySet.iterator(); // 迭代键的集合
10 while (it.hasNext()) {
11 Object key = it.next();
12 Object value = map.get(key); // 获取每个键所对应的值
13 System.out.println(key + ":" + value);
14 }
15 }
16 }
上述代码中,第8~14行代码是第一种遍历Map的方式。首先调用Map对象的KeySet()方法,获得存储Map中所有键的Set集合,然后通过Iterator迭代Set集合的每一个元素,即每一个键,最后通过调用get(String key)方法,根据键获取对应的值。
Map集合的另外一种遍历方式是先获取集合中的所有的映射关系,然后从映射关系中取出键和值。接下来通过一个案例演示这种遍历方式。
1 import java.util.*;
2 public class Example16 {
3 public static void main(String[] args) {
4 HashMap map = new HashMap(); // 创建Map集合
5 map.put("1", "张三"); // 存储键和值
6 map.put("2", "李四");
7 map.put("3", "王五");
8 Set entrySet = map.entrySet();
9 Iterator it = entrySet.iterator(); // 获取Iterator对象
10 while (it.hasNext()) {
11 // 获取集合中键值对映射关系
12 Map.Entry entry = (Map.Entry) (it.next());
13 Object key = entry.getKey(); // 获取Entry中的键
14 Object value = entry.getValue(); // 获取Entry中的值
15 System.out.println(key + ":" + value);
16 }
17 }
18 }
上述代码中,第8~15行代码是第二种遍历Map的方式。首先调用Map对象的entrySet()方法获得存储在Map中所有映射的Set集合,这个集合中存放了Map.Entry类型的元素(Entry是Map内部接口),每个Map.Entry对象代表Map中的一个键值对,然后迭代Set集合,获得每一个映射对象,并分别调用映射对象的getKey()和getValue()方法获取键和值。
在Map中,还提供了一些操作集合的常用方法,例如,values()方法用于得到map实例中所有的value,返回值类型为Collection;size()方法获取map集合类的大小;containsKey()方法用于判断是否包含传入的键;containsValue()方法用于判断是否包含传入的值;remove()方法用于根据key移除map中的与该key对应的value等。
接下来通过一个案例演示Map这些方法的使用。
1 import java.util.*;
2 public class Example17 {
3 public static void main(String[] args) {
4 HashMap map = new HashMap(); // 创建Map集合
5 map.put("1", "张三"); // 存储键和值
6 map.put("3", "李四");
7 map.put("2", "王五");
8 map.put("4", "赵六");
9 System.out.println("集合大小为:"+map.size());
10 System.out.println("判断是否包含传入的键:"+map.containsKey("2"));
11 System.out.println("判断是否包含传入的值:"+map.containsValue("王五"));
12 System.out.println("移除键为1的值是:"+map.remove("1"));
13 Collection values = map.values();
14 Iterator it = values.iterator();
15 while (it.hasNext()) {
16 Object value = it.next();
17 System.out.println(value);
18 }
19 }
20 }
上述代码中,第4~8行代码是声明了一个HashMap集合并通过Map的put(Object key,Object value)方法向集合中加入4个元素。第9行代码是通过Map的size()方法获取了集合的大小;第10~11行代码是通过containsKey(Object key)方法和containsValue(Object value)分别判断集合中是否包含所传入的键和值;第12行代码是通过remove(Object key)方法是删除键为1的元素对应的值;第13~18行代码是通过values()方法获取包含Map中所有值的Collection集合,然后通过迭代器输出集合中的每一个值。
从上面的例子可以看出,HashMap集合迭代出来元素的顺序和存入的顺序是不一致的。如果想让这两个顺序一致,可以使用Java中提供的LinkedHashMap类,它是HashMap的子类,与LinkedList一样,它也使用双向链表来维护内部元素的关系,使Map元素迭代的顺序与存入的顺序一致。
接下来通过一个案例学习LinkedHashMap的用法。
1 import java.util.*;
2 public class Example18 {
3 public static void main(String[] args) {
4 LinkedHashMap map = new LinkedHashMap(); // 创建Map集合
5 map.put("3", "李四"); // 存储键和值
6 map.put("2", "王五");
7 map.put("4", "赵六");
8 Set keySet = map.keySet();
9 Iterator it = keySet.iterator();
10 while (it.hasNext()) {
11 Object key = it.next();
12 Object value = map.get(key); // 获取每个键所对应的值
13 System.out.println(key + ":" + value);
14 }
15 }
16 }
上述代码中,第4~7行代码是创建了一个LinkedHashMap集合并存入了3个元素,第8~14行代码是使用迭代器遍历集合中的元素并通过元素的键获取对应的值,并打印。从运行结果可以看出,元素迭代出来的顺序和存入的顺序是一致的。
HashMap集合存储的元素的键值是无序的和不可重复的,为了对集合中的元素的键值进行排序,Map接口提供了另一个可以对集合中元素键值进行排序的类TreeMap。
接下来通过一个案例演示TreeMap集合的用法。
1 import java.util.Iterator;
2 import java.util.Set;
3 import java.util.TreeMap;
4 public class Example19 {
5 public static void main(String[] args) {
6 TreeMap map = new TreeMap(); // 创建Map集合
7 map.put(3, "李四");// 存储键和值
8 map.put(2, "王五");
9 map.put(4, "赵六");
10 map.put(3, "张三");
11 Set keySet = map.keySet();
12 Iterator it = keySet.iterator();
13 while (it.hasNext()) {
14 Object key = it.next();
15 Object value = map.get(key); // 获取每个键所对应的值
16 System.out.println(key+":"+value);
17 }
18 }
19 }
上述代码中,第6~10行代码是通过Map的put(Object key,Object value)方法向集合中加入4个元素;第11~17行代码是使用迭代器遍历集合中的元素并通过元素的键获取对应的值,并打印。从运行结果可以看出,添加的元素已经自动排序,并且键值重复存入的整数3只有一个,只是后边添加的值“张三”覆盖了原来的值“李四”。这也证实了TreeMap中的键必须是唯一的,不能重复并且有序,如果存储了相同的键,后存储的值则会覆盖原有的值。
TreeMap集合之所以可以对添加的元素的键值进行排序,其实现同TreeSet一样,TreeMap的排序也分自然排序与比较排序两种。接下来通过一个案例演示比较排序法实现按键值排序,在该案例中,键是自定义类值是String类。
1 import java.util.*;
2 class Student {
3 private String name;
4 private int age;
5 public String getName() {
6 return name;
7 }
8 public void setName(String name) {
9 this.name = name;
10 }
11 public int getAge() {
12 return age;
13 }
14 public void setAge(int age) {
15 this.age = age;
16 }
17 public Student(String name, int age) {
18 super();
19 this.name = name;
20 this.age = age;
21 }
22 @Override
23 public String toString() {
24 return "Student [name=" + name + ", age=" + age + "]";
25 }
26 }
27 public class Example20 {
28 public static void main(String[] args) {
29 TreeMap tm = new TreeMap(new Comparator() {
30 @Override
31 public int compare(Student s1, Student s2) {
32 int num = s1.getName().compareTo(s2.getName());//按照姓名比较
33 return num == 0 ? num:s1.getAge() - s2.getAge();
34 }
35 });
36 tm.put(new Student("张三", 23), "北京");
37 tm.put(new Student("李四", 13), "上海");
38 tm.put(new Student("赵六", 43), "深圳");
39 tm.put(new Student("王五", 33), "广州");
40 Set keySet = tm.keySet();
41 Iterator it = keySet.iterator();
42 while (it.hasNext()) {
43 Object key = it.next();
44 Object value = tm.get(key); // 获取每个键所对应的值
45 System.out.println(key+":"+value);
46 }
47 }
48 }
上述代码中,第2~26行代码定义了一个Student类;第29~35行代码定义了一个TreeMap集合,并在该集合中通过匿名内部类的方式实现了Comparator接口,然后重写了compare()方法,在compare()方法中通过三目运算符的方式自定义了排序方式为先按照年龄排序,年龄相同再按照姓名排序。第36~46行代码是通过Map的put(Object key,Object value)方法向集合中加入4个键为Student对象值为String类型的元素,并使用迭代器将集合中元素打印输出。
Map接口中还有一个实现类Hashtable,它和HashMap十分相似,区别在于Hashtable是线程安全的。Hashtable存取元素时速度很慢,目前基本上被HashMap类所取代,但Hashtable类有一个子类Properties在实际应用中非常重要。
Properties主要用来存储字符串类型的键和值,在实际开发中,经常使用Properties集合来存取应用的配置项。假设有一个文本编辑工具,要求默认背景色是红色,字体大小为14px,语言为中文,其配置项如下面的代码:
Backgroup-color = red Font-size = 14px Language = chinese
在程序中可以使用Properties集合对这些配置项进行存取,接下来通过一个案例学习Properties集合的使用。
1 import java.util.*;
2 public class Example21 {
3 public static void main(String[] args) {
4 Properties p=new Properties(); // 创建Properties对象
5 p.setProperty("Backgroup-color", "red");
6 p.setProperty("Font-size", "14px");
7 p.setProperty("Language", "chinese");
8 Enumeration names = p.propertyNames();//获取Enumeration对象所有键枚举
9 while(names.hasMoreElements()){ // 循环遍历所有的键
10 String key=(String) names.nextElement();
11 String value=p.getProperty(key); // 获取对应键的值
12 System.out.println(key+" = "+value);
13 }
14 }
15 }
上述代码的Properties类中,针对字符串的存取提供了两个专用的方法setProperty()和getProperty()。setProperty()方法用于将配置项的键和值添加到Properties集合当中。在第8行代码中通过调用Properties的propertyNames()方法得到一个包含所有键的Enumeration对象,然后在遍历所有的键时,通过调用getProperty()方法获得键所对应的值。
斗地主的扑克牌游戏,相信许多人都会玩,本例要求编写一个斗地主的洗牌发牌程序,要求按照斗地主的规则完成洗牌发牌的过程。一副扑克总共有54张牌,牌面由花色和数字组成(包括J、Q、K、A字母)组成,花色有♠、♥、♦、♣ 四种,分别表示黑桃、红桃、方块、梅花,小☺、大☻分别表示小王和大王。斗地主游戏共有三位玩家参与,首先将这54张牌的顺序打乱每人轮流摸一次牌,剩余3张留作底牌,然后在控制台打印三位玩家的牌和三张底牌。
(1)要实现纸牌程序,首先需要完成纸牌的组装。牌面是由花色(包括♠、♥、♦、♣花色)和数字(包括J、Q、K、A字母)两部分组成,可以创建两个ArrayList集合作为花色集合与数字集合,存储时需要注意。比10大的牌的数字用J、Q、K表示,1用A表示。
(2)将花色集合与数字集合这两个循环进行嵌套循环,将花色与数字组合,形成52章牌,并赋予其编号。将组合后的牌存放到一个HashMap集合中,集合的Key值是编号,value值是组装完成的纸牌。还有两张牌是大小王(小☺表示小王、大☻表示大王)。由于组装规则不一致,需单独使用add()方法将这两张牌加入到HashMap集合中。
(3)创建一个数字集合,用这个数字集合代替纸牌完成洗牌和发牌操作。由于纸牌的数量是54张,所以创建集合范围是0~53。
(4)可以使用Collection类的shuffle()方法完成打乱数字集合的操作,实现洗牌效果。由于只有3个人,所以可以使用for循环,通过将数字与3取余的方法,将代表不同纸牌的数字分配给不同人与底牌,实现发牌效果。
(5) 洗牌和发牌结束后,可以通过Collection类的sort()方法完成排序,之后通过foreach()循环HashMap集合,根据数字查找对应的纸牌字符串,并存入创建的字符串集合中,最后展示字符串集合。
1 package com.itheima;
2 import java.util.ArrayList;
3 import java.util.Collections;
4 import java.util.HashMap;
5 public class example5 {
6 public static void main(String[] args) {
7 // 准备花色
8 ArrayList color = new ArrayList();
9 color.add("♠");
10 color.add("♥");
11 color.add("♦");
12 color.add("♣");
13 // 准备数字 ,用ArrayList将纸牌由小到大排序
14 ArrayList number = new ArrayList();
15 for (int i = 3; i <= 10; i++) {
16 number.add(i + "");
17 }
18 number.add("J");
19 number.add("Q");
20 number.add("K");
21 number.add("A");
22 number.add("2");
23 // 定义一个map集合:用来将数字与每一张牌进行对应
24 HashMap map = new HashMap();
25 int index = 0;// 纸牌编号
26 for (String thisNumber : number) {// 循环纸牌数字
27 for (String thisColor : color) {// 循环纸牌花色
28 // 将花色与数字组合,形成52张牌,并赋予其编号
29 map.put(index++, thisColor + thisNumber);
30 }
31 }
32 // 加入大小王
33 map.put(index++, "小☺");
34 map.put(index++, "大☻");
35 // 一副54张的牌 ArrayList里边为0-53的数的新牌
36 ArrayList cards = new ArrayList();
37 for (int i = 0; i <= 53; i++) {
38 cards.add(i);// 此时的cards顺序为0-53
39 }
40 // 洗牌,使用Collections工具类中的shuffle()方法
41 Collections.shuffle(cards);// 此时的cards顺序已被打乱
42 // 创建三个玩家和底牌
43 ArrayList iPlayer = new ArrayList();
44 ArrayList iPlayer2 = new ArrayList();
45 ArrayList iPlayer3 = new ArrayList();
46 ArrayList iSecretCards = new ArrayList();
47 // 遍历这副洗好的牌,遍历过程中,将牌发到三个玩家和底牌中
48 for (int i = 0; i < cards.size(); i++) {
49 if (i >= 51) {
50 iSecretCards.add(cards.get(i));// 留取3张底牌
51 } else {
52 if (i % 3 == 0) {
53 iPlayer.add(cards.get(i));//与3取余为0的牌发给玩家1
54 } else if (i % 3 == 1) {
55 iPlayer2.add(cards.get(i));//与3取余为1的牌发给玩家2
56 } else {
57 iPlayer3.add(cards.get(i));// 其余的牌发给玩家3
58 }
59 }
60 }
61 // 对每个人手中的牌排序,使用Collections工具类中的sort()方法
62 Collections.sort(iPlayer);
63 Collections.sort(iPlayer2);
64 Collections.sort(iPlayer3);
65 // 对应数字形式的每个人手中的牌,定义字符串形式的牌
66 ArrayList sPlayer = new ArrayList();
67 ArrayList sPlayer2 = new ArrayList();
68 ArrayList sPlayer3 = new ArrayList();
69 ArrayList sSecretCards = new ArrayList();
70 // 循环主键,从map中获取纸牌
71 for (Integer key : iPlayer) {
72 sPlayer.add(map.get(key));
73 }
74 for (Integer key : iPlayer2) {
75 sPlayer2.add(map.get(key));
76 }
77 for (Integer key : iPlayer3) {
78 sPlayer3.add(map.get(key));
79 }
80 for (Integer key : iSecretCards) {
81 sSecretCards.add(map.get(key));
82 }
83 // 看牌
84 System.out.println("玩家1:" + sPlayer);
85 System.out.println("玩家2:" + sPlayer2);
86 System.out.println("玩家3:" + sPlayer3);
87 System.out.println("底牌:" + sSecretCards);
88 }
89 }
相信百度翻译对于大家来说并不陌生,本案例要求编写一个程序模拟百度翻译。用户输入英文之后搜索程序中对应的中文,如果搜索到对应的中文就输出搜索结果,反之给出提示。本案例要求使用Map集合实现英文与中文的存储。
(1)百度翻译主要用于翻译对应的意思。这是一种映射关系。因此可以用 Map集合来实现,所以首先就是定义Map集合,存储数据。
(2)用键盘录入功能获取我们要翻译的单词。
(3)定义一个方法,在该方法中实现对单词的查询操作,并且根据不同情况给出相关提示。
(4)调用查询的方法,实现翻译。并将结果输出到控制台。
2 import java.util.HashMap;
3 import java.util.Scanner;
4 public class example7 {
5 public static void main(String[] args) {
6 //定义Map集合用于存储词典的数据
7 HashMap word = new HashMap();
8 while(true) {
9 //集合合添加词典的数据。
10 word.put("apple", "苹果");
11 word.put("banner", "香蕉");
12 word.put("book", "书");
13 word.put("telephone", "电话");
14 word.put("cat", "猫");
15 word.put("dog", "狗");
16 word.put("student", "学生");
17 word.put("teacher", "老师");
18 //键盘录入功能获取我们要翻译的单词
19 Scanner sc = new Scanner(System.in);
20 System.out.println("请您输入您要翻译的单词:");
21 String w = sc.nextLine();
22 //调用判断打印输出
23 Select(w, word);
24 }
25 }
26 //定义方法对键盘录入的数据进行判断
27 public static String Select(String w, HashMap word) {
28 if(w.isEmpty()) {
29 System.out.println("请输入您要翻译的单词:");
30 return "";
31 }else if(!word.containsKey(w)) {
32 System.out.println("对不起,您要翻译的单词不存在,请重新输入:");
33 }else{
34 String chinese = word.get(w);
35 System.out.println(w+"翻译成中文意思为:"+chinese);
36 }
37 return w;
38 }
39 }
泛型是指定一个表示类型的变量,即“参数化类型”。在编程中用泛型来代替某个实际的类型,而后通过实际调用时传入或推导的类型来对泛型进行替换,以达到代码复用的目的。在使用泛型的过程中,操作数据类型被指定为一个参数,这种参数类型在类、接口和方法中,分别称为泛型类、泛型接口、泛型方法。
相对于传统上的形参,泛型可以使我们的参数具有更多类型上的变化,使代码能更好地复用。例如下面这段代码。
public class Box {
private String value;
public void set(String value) {
this.value = value;
}
public String get() {
return value;
}
}
上述代码中,定义了一个Box类,Box类中设置了一个 String 类型的数据。这时程序运行起来是没有问题的。但是,如果我们又需要一个能设置 Integer 类型数据的类,这个时候我们只能重新创建一个类,把 value 改为 Integer类型的。可是,随着业务不断增加,我们需要设置越来越多数据类型的类,这样会使得工程变得越来越“笨重”,并且安全性和重用性都非常低。
泛型就能够很好的解决上述问题。接下来,使用泛型改造Box类,具体代码如下所示。
public class Box {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
上述代码中, Box类在定义时使用了“
泛型类就是在类声明时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。只有用户使用该类的时候,该类所属的类型才能明确。
泛型类的声明格式具体如下: [访问权限] class 类名称<泛型类型标识1,泛型类型标识2,…,泛型类型标识n> [访问权限] 泛型类型标识 变量名称; [访问权限] 泛型类型标识 方法名称; [访问权限] 返回值类型声明 方法名称(泛型类型标识 变量名称){};
定义好泛型类之后,就可以创建泛型对象。创建泛型对象的语法格式具体如下: 类名称<参数化类型> 对象名称 = new 类名称<参数化类型>();
为了大家更好地理解泛型类以及泛型对象的使用,下面先看一个例子。
1 import java.util.*;
2 public class Example22 {
3 public static void main(String[] args) {
4 ArrayList list = new ArrayList(); // 创建ArrayList集合
5 list.add("String"); // 添加字符串对象
6 list.add(2);
7 list.add(1); // 添加Integer对象
8 for (Object obj : list) { // 遍历集合
9 String str = (String) obj; // 强制转换成String类型
10 }
11 }
12 }
在上述代码中,向List集合存入了3个元素,分别是一个字符串和两个int类型的整数。在取出这些元素时,都将它们强转为String类型,由于Integer对象无法转换为String类型,因此程序在运行时会出现上图所示错误。
接下来,对第4行代码进行修改,如下所示:
上面这种写法就限定了ArrayList集合只能存储Integer类型元素,改写后的程序在中编译时就会出现错误提示,如下图所示。
ArrayListlist = new ArrayList ();
上面这种写法就限定了ArrayList集合只能存储Integer类型元素,改写后的程序在中编译时就会出现错误提示,如下图所示。
修改之后的程序编译报错的原因是修改后的代码限定了集合元素的数据类型,ArrayList
使用泛型可以很好的解决上述问题。接下来,使用泛型再次对上述案例进行改写。
1 import java.util.*;
2 public class Example23 {
3 public static void main(String[] args) {
4 ArrayList list = new ArrayList();
5 list.add(1); // 添加字符串对象
6 list.add(2);
7 for (Integer str : list) { // 遍历集合
8 System.out.println(str);
9 }
10 }
11 }
上述代码中,使用泛型规定了ArrayList集合只能存入Integer类型元素,然后向集合中存入了两个Integer类型元素,并对这个集合进行遍历,从运行结果可以看出,该文件已经可以正常运行。需要注意的是,在使用泛型后,每次遍历集合元素时,可以指定元素类型为Integer,而不是Object,这样就避免了在程序中进行强制类型转换。
泛型方法的定义与其所在的类是否是泛型类是没有任何关系的,泛型方法所在的类可以是泛型类,也可以不是泛型类。定义泛型方法代码如下所示: [访问权限] <泛型标识> 返回值类型 方法名称(泛型标识 参数名称)
接下来通过一个案例来学习泛型方法的定义与使用。
1 public class Example24 {
2 public static void main(String[] args) {
3 //创建对象
4 Dog dog = new Dog();
5 //调用方法,传入的参数是什么类型,返回值就是什么类型
6 dog.show("hello");
7 dog.show(12);
8 dog.show(12.5);
9 }
10 }
11 class Dog{
12 String eat;
13 Integer age;
14 public void show(T t) {
15 System.out.println(t);
16 }
17 }
18 tool.show(12.5);
19 }
上述代码中,第14~16行代码定义了一个泛型方法show(),并将show()方法的参数类型和返回值类型规定为泛型,这样调用方法时,传入的参数是什么类型,返回值就是什么类型,如果定义为其他类型,传入参数就必须是方法指定的参数类型,否则编译就会出现错误。
在JDK5之后,不仅可以声明泛型类,也可以声明泛型接口,声明泛型接口和声明泛型类的语法类似,也是在接口名称后面加上
interface Info{ public T getVar(); }
泛型接口定义完成之后,就要定义此接口的子类,定义泛型接口的子类有两种方式,一种是直接在子类实现的接口中明确地给出泛型类型,另一种是直接在子类后声明泛型。
1.直接在接口中指定具体类型 当子类明确泛型类的类型参数变量时,外界使用子类的时候,需要传递类型参数变量进来,在实现类中需要定义出类型参数变量。接下来通过一个案例学习这种情况的泛型接口定义。
首先定义一个泛型接口:
public interface Inter {
public abstract void show(T t);
}
接着定义泛型接口的子类:
public class InterImpl implements Inter {
@Override
public void show(String s) {
System.out.println(s);
}
}
最后定义实现类,进行测试:
public class Example25 {
public static void main(String[] args) {
Inter inter = new InterImpl();
inter.show("hello");
}
}
上述代码中定义了一个泛型接口Inter,在泛型接口子类InterImpl中实现了Inter接口。InterImpl实现 Inter接口时,直接在实现的接口处制定了具体的泛型类型String,这样在重写Inter接口中的show()方法时直接指明类型为String即可。
2.在子类的定义上声明泛型类型 当子类不明确泛型类的类型参数变量,外界使用子类的时候,也需要传递类型参数变量进来,在实现类中也需要定义出类型参数变。接下来通过修改子类InterImpl和测试程序来学习这种情况的泛型接口定义。
泛型接口子类InterImpl的实现如下:
public class InterImpl implements Inter {
@Override
public void show(T t) {
System.out.println(t);
}
}
测试代码实现如下:
public class Example25 {
public static void main(String[] args) {
Inter inter = new InterImpl();
inter.show("hello");
Inter ii = new InterImpl<>();
ii.show(12);
}
}
在Java中,数组是可以协变的,例如,Dog extends Animal,那么Animal[]与dog[]是可以兼容的。而集合是不能协变的,也就是说List
下面我们通过一个案例演示通配符的使用。
1 import java.util.*;
2 public class Example28 {
3 public static void main(String[] args) {
4 //List集合装载的是Integer
5 List list = new ArrayList<>();
6 list.add(1);
7 list.add(2);
8 test(list);
9 }
10 public static void test(List> list) {
11 for(int i=0;i
需要注意的是,如果使用通配符“?”接收泛型对象,则通配符“?”修饰的对象只能接收,不能修改,也就是不能设置。错误的代码如下所示。
通配符表示可以匹配任意类型,任意的Java类都可以匹配, 但是当接收一个List集合时,它只能操作数字类型的元素(Float、Integer、Double、Byte等数字类型都行),而如果直接使用通配符的话,该集合就不是只能操作数字了。针对这类问题我们可以设定通配符的上限和下限。 设定通配符上限代码如下所示: List extends Number> 设定通配符下限代码如下所示: super Type>
Lambda表达式是JDK8的一个新特性,Lambda可以取代大部分的匿名内部类,写出更优雅的Java代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。JDK也提供了大量的内置函数式接口供我们使用,使得 Lambda 表达式的运用更加方便、高效。
Lambda表达式由参数列表、箭头符号 -> 和函数体组成。函数体既可以是一个表达式,也可以是一个语句块。其中达式会被执行然后返回执行结果;语句块中的语句会被依次执行,就像方法中的语句一样。
Lambda表达式常用的语法格式如下表。
语法格式 | 描述 |
---|---|
()-> System.out.println("Hello Lambda!"); | 无参数,无返回值 |
(x) -> System.out.println(x) | 有一个参数,并且无返回值 |
x -> System.out.println(x) | 若只有一个参数,小括号可以省略不写 |
Comparator |
有两个以上的参数,有返回值,并且 Lambda 体中有多条语句 |
Comparator |
若Lambda 体中只有一条语句,return 和大括号都可以省略不写 |
(Integer x, Integer y) -> Integer.compare(x, y); | Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,即“类型推断” |
接下来通过一个案例来学习Lambda表达式语法。
1 import java.util.Arrays;
2 public class Example29 {
3 public static void main(String[] args) {
4 String[] arr = {"program", "creek", "is", "a", "java", "site"};
5 Arrays.sort(arr, (m, n) -> Integer.compare(m.length(), n.length()));
6 System.out.println("Lambda语句体中只有一条语句,参数类型可推断:"+
7 Arrays.toString(arr));
8 Arrays.sort(arr, (String m, String n) -> {
9 if (m.length() > n.length())
10 return -1;
11 else
12 return 0;
13 });
14 System.out.println("Lambda语句体中有多条语句:"+Arrays.toString(arr));
15 }
16 }
上述代码中,第4行代码定义了一个字符串数组arr,第5~13行代码使用了两种Lambda表达式语法对字符串数组arr进行了排序。其中,第5行代码是用compare()方法比较字符串的长度来进行排序,第8~13行代码是使用if … else语法比较字符串的长度来进行排序。
本章详细介绍了几种Java常用集合类,首先介绍了集合的概念和Collection接口;其次介绍了List接口,包括ArrayList、LinkedList、Iterator和foreach循环;接着介绍了Set接口,包HashSet集合和TreeSet集合;然后Map接口,包括HashMap和TreeMap;最后介绍了泛型,包括泛型类、泛型对象、泛型接口和类型通配符。最后还介绍了JDK8的一个新特性——Lambda表达式。通过本章的学习,读者可以熟练掌握各种集合类的使用场景,以及需要注意的细节,同时可以掌握泛型与 Lambda表达式的使用。