集合概述和分类
Java还提供了很多种其他的集合,如下图所示:
Collection单列集合
Collection是单列集合的根接口,Collection接口下面又有两个子接口List接口、Set接口,List和Set下面分别有不同的实现类,如下图所示:
上图中各种集合的特点如下图所示:
- List系列集合:添加元素是有序、可重复、有索引。
- ArrayList、LinkedList:有序、可重复、有索引。
- Set系列集合:添加的元素是无序、不重复、无索引。
- HashSet:无序、不重复、无索引;
- LinkedHashSet:有序、不重复、无索引。
- TreeSet:按照大小默认升序排序、不重复、无索引
Collection集合的特点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ArrayList<String> list = new ArrayList<>(); list.add("java1"); list.add("java2"); list.add("java1"); list.add("java2"); System.out.println(list);
HashSet<String> list = new HashSet<>(); list.add("java1"); list.add("java2"); list.add("java1"); list.add("java2"); list.add("java3"); System.out.println(list);
|
Collection集合的常用方法
Collection集合的常用功能如下,ArrayList, LinkedList, HashSet, LinkedHashSet, TreeSet
集合都可以调用下面的方法。
方法名 |
说明 |
public boolean add(Ee) |
把给定的对象添加到当前集合中 |
public void clear() |
清空集合中所有的元素 |
public boolean remove(E e) |
把给定的对象在当前集合中删除 |
public boolean contains(Object obj) |
判断当前集合中是否包含给定的对象 |
public boolean isEmpty() |
判断当前集合是否为空 |
public int size() |
返回集合中元素的个数 |
public Object[] toArray() |
把集合中的元素,存储到数组中 |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| Collection<String> c = new ArrayList<>();
c.add("java1"); c.add("java1"); c.add("java2"); c.add("java2"); c.add("java3"); System.out.println(c);
System.out.println(c.size());
System.out.println(c.contains("java1")); System.out.println(c.contains("Java1"));
System.out.println(c.remove("java1")); System.out.println(c);
c.clear(); System.out.println(c);
System.out.println(c.isEmpty());
Object[] array = c.toArray(); System.out.println(Arrays.toString(array));
String[] array1 = c.toArray(new String[c.size()]); System.out.println(Arrays.toString(array1));
Collection<String> c1 = new ArrayList<>(); c1.add("java1"); c1.add("java2"); Collection<String> c2 = new ArrayList<>(); c2.add("java3"); c2.add("java4"); c1.addAll(c2); System.out.println(c1);
|
Collection遍历方式
因为for循环只能遍历List集合,不能遍历Set集合,普通for循环遍历需要索引,只有List集合有索引,而Set集合没有索引。所以我们需要有一种通用的遍历方式,能够遍历所有集合。
迭代器遍历集合
迭代器就是一种集合的通用遍历方式。
迭代器概述:迭代器是用来遍历集合的专用方式(数组没有迭代器),在]ava中迭代器的代表是Iterator
。
代码写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Collection<String> c = new ArrayList<>(); c.add("Josh"); c.add("Ben"); c.add("Bob"); c.add("Alex"); System.out.println(c);
Iterator<String> it = c.iterator();
while(it.hasNext()){ String e = it.next(); System.out.println(s); }
|
迭代器代码的原理如下:
- 当调用
iterator()
方法获取迭代器时,当前指向第一个元素
hasNext()
方法则判断这个位置是否有元素,如果有则返回true,进入循环
- 调用
next()
方法获取元素,并将当月元素指向下一个位置,
- 等下次循环时,则获取下一个元素,依此内推
使用迭代器遍历集合用到的方法:
方法名称 |
说明 |
Iterator<E> iterator() |
返回集合中的迭代器对象,该迭代器对象默认指向当前集合的第一个元素 |
Iterator迭代器中的常用方法
方法名称 |
说明 |
boolean hasNext() |
询问当前位置是否有元素存在,存在返回true,不存在返回false |
E next() |
获取当前位置的元素,并同时将迭代器对象指向下一个元素处。 |
增强for遍历集合
- 增强for可以用来遍历集合或者数组
- 增强for遍历集合,本质就是迭代器遍历集合的简化写法。
增强for循环格式如下:
1 2 3
| for(String str : Collection<String>) { System.out.println(str); }
|
forEach遍历集合
在JDK8版本以后还提供了一个forEach
方法也可以遍历集合,如果下图所示:
方法名称 |
说明 |
default void forEach(Consumer<? super T> action) |
结合lambda遍历集合 |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Collection<String> c = new ArrayList<>(); c.add("Josh"); c.add("Ben"); c.add("Bob"); c.add("Alex");
c.forEach(new Consumer<String>{ @Override public void accept(String s){ System.out.println(s); } });
c.forEach(s->System.out.println(s));
|
List系列集合
List系列集合特点:有序,可重复,有索引
ArrayList
:有序,可重复,有索引。
LinkedList
:有序,可重复,有索引。
底层采用的数据结构不同,实现不同 ! 适合的场是不同 !
List集合的常用方法
List集合是索引的,所以多了一些有索引操作的方法,如下图所示:
方法名称 |
说明 |
void add(int index, E element) |
在此集合中的指定位置插入指定的元素 |
E remove(int index) |
删除指定索引处的元素,返回被删除的元素 |
E set(int index, E element) |
修改指定索引处的元素,返回被修改的元素 |
E get(int index) |
返回指定索引处的元素 |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| List<String> list = new ArrayList<>(); list.add("Josh"); list.add("Ben"); list.add("Ben"); list.add("Philips"); System.out.println(list);
list.add(2, "Ori"); System.out.println(list);
System.out.println(list.remove(2)); System.out.println(list);
System.out.println(list.get(3));
System.out.println(list.set(3,"php")); System.out.println(list);
|
List集合的遍历方式
List集合相比于前面的Collection多了一种可以通过索引遍历的方式,所以List集合遍历方式一共有四种:
- 普通for循环(只因为List有索引)
- 迭代器
- 增强for
- Lambda表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| List<String> list = new ArrayList<>(); list.add("Josh"); list.add("Ben"); list.add("Alex");
for(int i = 0; i < list.size(); i++){ String e = list.get(i); System.out.println(e); }
for(String s : list){ System.out.println(s); }
Iterator<String> it = list.iterator(); while(it.hasNext()){ String s = it.next(); System.out.println(s); }
list.forEach(s -> System.out.println(s));
|
ArrayList底层的原理
ArrayList集合底层是基于数组结构实现的,也就是说当你往集合容器中存储元素时,底层本质上是往数组中存储元素。 特点如下:
- 查询速度快(注意:是根据索引查询数据快): 查询数据通过地址值和索引定位,查询任意数据耗时相同
- 删除效率低:可能需要把后面很多的数据进行前移
- 添加效率极低:可能需要把后面很多的数据后移,再添加元素,或者也可能需要进行数组的扩容
我们知道数组的长度是固定的,但是集合的长度是可变的,这是怎么做到的呢?原理如下:
- 利用无参构造器创建的集合,会在底层创建一个默认长度为0的数组
- 添加第一个元素时,底层会创建一个新的长度为10的数组
- 存满时,会扩容1.5倍
- 如果一次添加多个元素,1.5倍还放不下,则新创建数组的长度以实际为准
数组扩容,并不是在原数组上扩容(原数组是不可以扩容的),底层是创建一个新数组,然后把原数组中的元素全部复制到新数组中去。
ArrayList集合的应用场景
ArrayList适合
根据索引来查询数据,比如根据随机索引来取数据。这样是很高效的。或者是数据量不是很大的时候。
ArrayList不适合
数据量大的同时,又要频繁的进行增删操作。
LinkedList底层原理
接下来我们看一下LinkedList集合的底层原理。
LinkedList底层是链表结构,链表结构是由一个一个的节点组成,一个节点由数据值、下一个元素的地址组成。如下图所示:
链表中的结点是独立的对象,在内存中是不连续的,每个结点包含数据值和下一个结点的地址。
链表的特点1:查询慢,无论查询哪个数据都要从头开始找,
链表的特点2:链表增删相对快
假如,现在要在B节点和D节点中间插入一个元素,只需要把B节点指向D节点的地址断掉,重新指向新的节点地址就可以了。如下图所示:
假如,现在想要把D节点删除,只需要让C节点指向E节点的地址,然后把D节点指向E节点的地址断掉。此时D节点就会变成垃圾,会把垃圾回收器清理掉。
上面的链表是单向链表,它的方向是从头节点指向尾节点的,只能从左往右查找元素,这样查询效率比较慢;还有一种链表叫做双向链表,不光可以从做往右找,还可以从右往左找。如下图所示:
双向链表特点:查询慢(相对于数组),增删相对较快,但对【首尾】元素进行增删改查的速度是【极快】的
LinkedList集合是基于双向链表实现了,所以相对于ArrayList,LinkedList新增了一些可以针对头尾进行操作的方法,如下所示:
方法名称 |
说明 |
public void addFirst(E e) |
在该列表开头插入指定的元素 |
public void addLast(E e) |
将指定的元素追加到此列表的末尾 |
public E getFirst() |
返回此列表中的第一个元素 |
public E getLast() |
返回此列表中的最后一个元素 |
public E removeFirst() |
从此列表中删除并返回第一个元素 |
public E removeLast() |
从此列表中删除并返回最后一个元素 |
LinkedList集合的应用场景
LInkedList集合有什么用呢?可以用它来设计栈结构、队列结构。
- 我们先来认识一下队列结构,队列结构你可以认为是一个上端开口,下端也开口的管子的形状。元素从上端入队列,从下端出队列。先进先出,后进后出。
入队列可以调用LinkedList
集合的addLast
方法,出队列可以调用removeFirst
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| LinkedList<String> queue = new LinkedList<>();
queue.addLast("第1号人"); queue.addLast("第2号人"); queue.addLast("第3号人"); queue.addLast("第4号人"); System.out.println(queue);
System.out.println(queue.removeFirst()); System.out.println(queue.removeFirst()); System.out.println(queue.removeFirst()); System.out.println(queue.removeFirst());
|
- 接下来,我们再用LinkedList集合来模拟一下栈结构的效果。还是先来认识一下栈结构长什么样。栈结构可以看做是一个上端开头,下端闭口的水杯的形状。
- 元素永远是上端进,也从上端出,先进入的元素会压在最底下,所以栈结构的特点是先进后出,后进先出
我们可以把栈理解成一个弹匣,子弹压入弹匣,是先压入的子弹在最底层,后压入的子弹在最顶层。而且射击也是先从最上层开始。
接着,我们就用LinkedList来模拟下栈结构,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| LinkedList<String> stack = new ArrayList<>();
stack.push("第1颗子弹"); stack.push("第2颗子弹"); stack.push("第3颗子弹"); stack.push("第4颗子弹"); System.out.println(stack);
System.out.println(statck.pop()); System.out.println(statck.pop()); System.out.println(statck.pop()); System.out.println(statck.pop());
System.out.println(list);
|
Set系列集合
Set集合是属于Collection体系下的另一个分支,它的特点如下所示:
Set系列集合特点:无序,添加数据的顺序和获取出的数据顺序不一致,不重复,无索引
- HashSet:无序、不重复、无索引。
- LinkedHashSet:有序、不重复、无索引。
- TreeSet:排序、不重复、无索引
下面我们用代码简单演示一下,每一种Set集合的特点。
1 2 3 4 5 6 7 8 9 10 11
|
Set<Integer> set = new TreeSet<>(); set.add(666); set.add(555); set.add(555); set.add(888); set.add(888); set.add(777); set.add(777); System.out.println(set);
|
HashSet集合底层原理
哈希值
就是一个int类型的数值,Java中每个对象都有一个哈希值
Java中的所有对象,都可以调用obejct类提供的hashCode方法,返回该对象自己的哈希值
public int hashCode(): 返回对象的哈希码值
对象哈希值的特点
- 同一个对象多次调用hashCode()方法返回的哈希值是相同的
- 不同的对象,它们的哈希值一般不相同,但也有可能会相同(哈希碰撞)
HashSet集合底层是基于哈希表实现的,哈希表根据JDK版本的不同,也是有点区别的
- JDK8以前:哈希表 = 数组+链表
- JDK8以后:哈希表 = 数组+链表+红黑树
- 创建一个默认长度16的数组,默认加载因子为0.75,数组名table
- 使用元素的哈希值对数组的长度求余计算出应存入的位置
- 判断当前位置是否为null,如果是null直接存入
- 如果不为null,表示有元素,则调用equals方法比较:相等,则不存。不相等,则存入数组
- JDK 8之前,新元素存入数组,占老元素位置,老元素挂下面
- JDK 8开始之后,新元素直接挂在老元素下面
我们发现往HashSet集合中存储元素时,底层调用了元素的两个方法:一个是hashCode方法获取元素的hashCode值(哈希值);另一个是调用了元素的equals方法,用来比较新添加的元素和集合中已有的元素是否相同。
- 只有新添加元素的hashCode值和集合中以后元素的hashCode值相同、新添加的元素调用equals方法和集合中已有元素比较结果为true, 才认为元素重复。
- 如果hashCode值相同,equals比较不同,则以链表的形式连接在数组的同一个索引为位置(如上图所示)
- 哈希表是一种增删改查数据性能都较好的结构
在JDK8开始后,为了提高性能,当链表的长度超过8时,就会把链表转换为红黑树,如下图所示:
JDK8之前如果数组快占满了,会导致链表过长,从而导致查询性能降低。
红黑树:一个节点可以挂多个数据,数据由小到大从左往右排列。
二叉树
二叉树是一种数据结构,如下图所示:
二叉树中每一个节点中包含如下数值:
每个节点包含:值、含父节点地址、、左子节点地址、右子节点地址。
在如上图中的普通二叉树的实际使用的地方不多,我们主要要关注:二叉查找树(二叉排序树)。
二叉排序树的规则是:小的存在左边,大的存右边,一样的不存。
二又查找树存在的问题:
当数据已经是排好序的,导致查询的性能与单链表一样,查询速度变慢!
平衡二叉树
在满足查找二叉树的大小规则下,让树尽可能矮小,以此提高查数据的性能。同时红黑树也是一种自平衡二叉树。
HashSet去重原理
前面我们学习了HashSet存储元素的原理,依赖于两个方法:一个是hashCode方法用来确定在底层数组中存储的位置,另一个是用equals方法判断新添加的元素是否和集合中已有的元素相同。
要想保证在HashSet集合中没有重复元素,我们需要重写元素类的hashCode和equals方法。比如以下面的Student类为例,假设把Student类的对象作为HashSet集合的元素,想要让学生的姓名和年龄相同,就认为元素重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class Student{ private String name; private int age; private double height; public Student(){} public Student(String name, int age, double height){ this.name=name; this.age=age; this.height=height; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (age != student.age) return false; if (Double.compare(student.height, height) != 0) return false; return name != null ? name.equals(student.name) : student.name == null; }
@Override public int hashCode() { int result; long temp; result = name != null ? name.hashCode() : 0; result = 31 * result + age; temp = Double.doubleToLongBits(height); result = 31 * result + (int) (temp ^ (temp >>> 32)); return result; } }
|
接着,写一个测试类,往HashSet集合中存储Student对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class Test{ public static void main(String[] args){ Set<Student> students = new HashSet<>(); Student s1 = new Student("Josh",20, 169.6); Student s2 = new Student("Ben",23, 169.6); Student s3 = new Student("Ben",23, 169.6); Student s4 = new Student("Philips",48, 169.6); students.add(s1); students.add(s2); students.add(s3); students.add(s4); for(Student s : students){ System.out.println(s); } } }
|
打印结果如下,我们发现存了两个蜘蛛精,当时实际打印出来只有一个,而且是无序的。
1 2 3
| Student{name='Philips', age=48, height=169.6} Student{name='Josh', age=20, height=169.6} Student{name='Ben', age=23, height=169.6}
|
LinkedHashSet底层原理
HashSet的子类LinkedHashSet类。LinkedHashSet它底层采用的是也是哈希表结构,只不过额外新增了一个双向链表来维护元素的存取顺序。如下下图所示:
- 依然是基于哈希表(数组、链表、红黑树)实现的。
- 但是,它的每个元素都额外的多了一个双链表的机制记录它前后元素的位置
每次添加元素,就和上一个元素用双向链表连接一下。第一个添加的元素是双向链表的头节点,最后一个添加的元素是双向链表的尾节点。
把上个案例中的集合改成LinkedList集合,我们观察效果怎样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class Test{ public static void main(String[] args){ Set<Student> students = new LinkedHashSet<>(); Student s1 = new Student("Josh",20, 169.6); Student s2 = new Student("Ben",23, 169.6); Student s3 = new Student("Ben",23, 169.6); Student s4 = new Student("Philips",48, 169.6); students.add(s1); students.add(s2); students.add(s3); students.add(s4); for(Student s : students){ System.out.println(s); } } }
|
打印结果如下
1 2 3
| Student{name='Josh', age=20, height=169.6} Student{name='Ben', age=23, height=169.6} Student{name='Philips', age=48, height=169.6}
|
TreeSet集合
TreeSet集合的特点是可以对元素进行排序,但是必须指定元素的排序规则。
如果往集合中存储String类型的元素,或者Integer类型的元素,它们本身就具备排序规则,所以直接就可以排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Set<Integer> set1= new TreeSet<>(); set1.add(8); set1.add(6); set1.add(4); set1.add(3); set1.add(7); set1.add(1); set1.add(5); set1.add(2); System.out.println(set1);
Set<Integer> set2= new TreeSet<>(); set2.add("a"); set2.add("c"); set2.add("e"); set2.add("b"); set2.add("d"); set2.add("f"); set2.add("g"); System.out.println(set1);
|
如果往TreeSet集合中存储自定义类型的元素,比如说Student类型,则需要我们自己指定排序规则,否则会出现异常。
我们想要告诉TreeSet集合按照指定的规则排序,有两种办法:
- 让元素的类实现Comparable接口,重写compareTo方法
- 在创建TreeSet集合时,通过构造方法传递Compartor比较器对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public class Student implements Comparable<Student>{ private String name; private int age; private double height; public Student(){} public Student(String name, int age, double height){ this.name=name; this.age=age; this.height=height; }
@Override public int compareTo(Student o) { return this.age-o.age; } }
|
此时,再运行测试类,结果如下
1 2 3 4
| Student{name='Josh', age=20, height=169.6} Student{name='Ben', age=20, height=169.8} Student{name='Philips', age=23, height=169.6} Student{name='Alex', age=48, height=169.6}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
Set<Student> students = new TreeSet<>(new Comparator<Student>{ @Override public int compare(Student o1, Student o2){ return Double.compare(o1,o2); } });
Student{name='Josh', age=20, height=169.6} Student{name='Ben', age=20, height=169.8} Student{name='Philips', age=23, height=169.6} Student{name='Alex', age=48, height=169.6}
students.add(s1); students.add(s2); students.add(s3); students.add(s4); System.out.println(students);
|
补充:集合并发修改异常
学完Collection集合后,还有一个小问题需要补充说明一下,那就是在使用迭代器遍历集合时,可能存在并发修改异常。
我们先把这个异常用代码演示出来,再解释一下为什么会有这个异常产生
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| List<String> list = new ArrayList<>(); list.add("王麻子"); list.add("小李子"); list.add("李爱花"); list.add("张全蛋"); list.add("晓李"); list.add("李玉刚"); System.out.println(list);
Iterator<String> it = list.iterator(); while(it.hasNext()){ String name = it.next(); if(name.contains("李")){ list.remove(name); } } System.out.println(list);
|
运行上面的代码,会出现下面的异常。这就是并发修改异常
为什么会出现这个异常呢?那是因为迭代器遍历机制,规定迭代器遍历集合的同时,不允许集合自己去增删元素,否则就会出现这个异常。
怎么解决这个问题呢?不使用集合的删除方法,而是使用迭代器的删除方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| List<String> list = new ArrayList<>(); list.add("王麻子"); list.add("小李子"); list.add("李爱花"); list.add("张全蛋"); list.add("晓李"); list.add("李玉刚"); System.out.println(list);
Iterator<String> it = list.iterator(); while(it.hasNext()){ String name = it.next(); if(name.contains("李")){ it.remove(); } } System.out.println(list);
|
Collection系列集合总结
- 如果希望记住元素的添加顺序,需要存储重复的元素,又要频繁的根据索引查询数据?
- 用ArrayList集合(有序、可重复、有索引),底层基于数组的。 (常用)
- 如果希望记住元素的添加顺序,且增删首尾数据的情况较多?
- 用LinkedList集合(有序、可重复、有索引),底层基于双链表实现的。
- 如果不在意元素顺序,也没有重复元素需要存储,只希望增删改查都快?
- 用HashSet集合(无序,不重复,无索引),底层基于哈希表实现的。 (常用)
- 如果希望记住元素的添加顺序,也没有重复元素需要存储,且希望增删改查都快?
- 用LinkedHashSet集合(有序,不重复,无索引),底层基于哈希表和双链表
- 如果要对元素进行排序,也没有重复元素需要存储?且希望增删改查都快?
Collection的其他操作
前面我们已经把Collection家族的集合都学习完了。为了更加方便的对Collection集合进行操作,今天我们还要学习一个操作Collection集合的工具类,叫做Collections。但是Collections工具类中需要用到一个没有学过的小知识点,叫做可变参数,所以必须先学习这个前置知识可变参数,再学习Collections工具类
可变参数
首先,我们来学习一下可变参数。关于可变参数我们首先要知道它是什么,然后要知道它的本质。搞清楚这两个问题,可变参数就算你学明白了。
接下来,我们编写代码来演示一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class ParamTest{ public static void main(String[] args){ test(); test(10,20,30); int[] arr = new int[]{10,20,30,40} test(arr); } public static void test(int...nums){ System.out.println(nums.length); System.out.println(Arrays.toString(nums)); System.out.println("----------------"); } }
|
最后还有一些错误写法,需要让大家写代码时注意一下,不要这么写哦!!!
- 一个形参列表中,只能有一个可变参数;否则会报错
- 一个形参列表中如果多个参数,可变参数需要写在最后;否则会报错
Collections工具类
有了可变参数的基础,我们再学习Collections这个工具类就好理解了,因为这个工具类的方法中会用到可变参数。
注意Collections并不是集合,它比Collection多了一个s,一般后缀为s的类很多都是工具类。这里的Collections是用来操作Collection的工具类。它提供了一些好用的静态方法,如下
方法名称 |
说明 |
public static <T> boolean addAl1(Collection‹? super T> c, T... elements) |
给集合批量添加元素 |
public static void shuffle(List‹?> list) |
打乱List集合中的元素顺序 |
public static <T> void sort(List‹T> list) |
对List集合中的元素进行升序排序 |
public static <T> void sort(List‹T> list, Comparator‹? super T> c) |
对List集合中元素,按照比较器对象指定的规则进行排序 |
我们把这些方法用代码来演示一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class CollectionsTest{ public static void main(String[] args){ List<String> names = new ArrayList<>(); Collections.addAll(names, "张三","王五","李四", "张麻子"); System.out.println(names); Collections.shuffle(names); System.out.println(names); List<Integer> list = new ArrayList<>(); list.add(3); list.add(5); list.add(2); Collections.sort(list); System.out.println(list); } }
|
上面我们往集合中存储的元素要么是Stirng类型,要么是Integer类型,他们本来就有一种自然顺序所以可以直接排序。但是如果我们往List集合中存储Student对象,这个时候想要对List集合进行排序自定义比较规则的。指定排序规则有两种方式,如下:
排序方式1:让元素实现Comparable接口,重写compareTo方法
比如现在想要往集合中存储Studdent对象,首先需要准备一个Student类,实现Comparable接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Student implements Comparable<Student>{ private String name; private int age; private double height; @Override public int compareTo(Student o){ return this.age - o.age; } }
|
然后再使用Collections.sort(list集合)
对List集合排序,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| List<Student> students = new ArrayList<>(); students.add(new Student("Josh",23,169.7)); students.add(new Student("Ben",22,169.8)); students.add(new Student("Ben",22,169.8)); students.add(new Student("Philips",26,169.5));
Collections.sort(students); System.out.println(students);
|
排序方式2:使用调用sort方法是,传递比较器
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
Collections.sort(students, new Comparator<Student>(){ @Override public int compare(Student o1, Student o2){ return o1.getAge()-o2.getAge(); } }); System.out.println(students);
|
斗地主案例
仅先完成创建牌组、洗牌、捋牌、看牌的业务逻辑。我们先分析一下业务需求:
- 总共有54张牌,每一张牌有花色和点数两个属性、为了排序还可以再加一个序号
- 点数可以是:
“3”,"4","5","6","7","8","9","10","J","Q","K","A","2"
- 花色可以是:
“♣”,"♠","♥","♦"
- 斗地主时:三个玩家没人手里17张牌,剩余3张牌作为底牌
实现思路:
第一步:为了表示每一张牌有哪些属性,首先应该新建一个扑克牌的类
第二步:启动游戏时,就应该提前准备好54张牌
第三步:接着再完全洗牌、发牌、捋牌、看牌的业务逻辑
先来完成第一步,定义一个扑克类Card
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public class Card { private String number; private String color; private int size;
public Card() { }
public Card(String number, String color, int size) { this.number = number; this.color = color; this.size = size; }
public String getNumber() { return number; }
public void setNumber(String number) { this.number = number; }
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public int getSize() { return size; }
public void setSize(int size) { this.size = size; }
@Override public String toString() { return color + number ; } }
|
再完成第二步,定义一个房间类,初始化房间时准备好54张牌
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class Room { private List<Card> allCards = new ArrayList<>();
public Room(){ String[] numbers = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"}; String[] colors = {"♠", "♥", "♣", "♦"}; int size = 0; for (String number : numbers) { size++; for (String color : colors) { Card c = new Card(number, color, size); allCards.add(c); } } Card c1 = new Card("", "🃏" , ++size); Card c2 = new Card("", "👲" , ++size); Collections.addAll(allCards, c1, c2); System.out.println("新牌:" + allCards); } }
|
最后完成第三步,定义一个启动游戏的方法,完成洗牌、发牌、捋牌、看牌的业务逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
|
public void start() { Collections.shuffle(allCards); System.out.println("洗牌后:" + allCards);
List<Card> linHuChong = new ArrayList<>(); List<Card> jiuMoZhi = new ArrayList<>(); List<Card> renYingYing = new ArrayList<>(); for (int i = 0; i < allCards.size() - 3; i++) { Card c = allCards.get(i); if(i % 3 == 0){ linHuChong.add(c); }else if(i % 3 == 1){ jiuMoZhi.add(c); }else if(i % 3 == 2){ renYingYing.add(c); } }
sortCards(linHuChong); sortCards(jiuMoZhi); sortCards(renYingYing); System.out.println("啊冲:" + linHuChong); System.out.println("啊鸠:" + jiuMoZhi); System.out.println("盈盈:" + renYingYing); List<Card> lastThreeCards = allCards.subList(allCards.size() - 3, allCards.size()); System.out.println("底牌:" + lastThreeCards); jiuMoZhi.addAll(lastThreeCards); sortCards(jiuMoZhi); System.out.println("啊鸠抢到地主后:" + jiuMoZhi); }
private void sortCards(List<Card> cards) { Collections.sort(cards, new Comparator<Card>() { @Override public int compare(Card o1, Card o2) { return o2.getSize() - o1.getSize(); } }); }
|
不要忘记了写测试类了,
1 2 3 4 5 6 7 8 9
| public class GameDemo { public static void main(String[] args) { Room m = new Room(); m.start(); } }
|
Map集合
Map概述体系
前面我们已经把单列集合学习完了,接下来我们要学习的是双列集合。首先我们还是先认识一下什么是双列集合。
所谓双列集合,就是说集合中的元素是一对一对的。Map集合中的每一个元素是以key=value
的形式存在的,一个key=value
就称之为一个键值对,而且在Java中有一个类叫Entry类,Entry的对象用来表示键值对对象。
所有的Map集合有如下的特点:键不能重复,值可以重复,每一个键只能找到自己对应的值。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class MapTest1 { public static void main(String[] args) { Map<String, Integer> map = new LinkedHashMap<>(); map.put("手表", 100); map.put("手表", 220); map.put("手机", 2); map.put("Java", 2); map.put(null, null); System.out.println(map);
Map<Integer, String> map1 = new TreeMap<>(); map1.put(23, "Java"); map1.put(23, "MySQL"); map1.put(19, "李四"); map1.put(20, "王五"); System.out.println(map1); } }
|
Map集合也有很多种,在Java中使用不同的类来表示的,每一种Map集合其键的特点是有些差异的,值是键的一个附属值,所以我们只关注键的特点就可以了。
Map集合体系的特点
注意:Map系列集合的特点都是由键决定的,值只是一个附属品,值是不做要求的
- HashMap(由键决定特点):无序、不重复、无索引。(用的最多)
- LinkedHashMap(由键决定特点):由键决定的特点:有序、不重复、无索引。
- TreeMap(由键决定特点):按照大小默认升序排序、不重复、无素引。
Map集合的常用方法
我们只需要学习Map接口中每一个方法是什么含义,那么所有的Map集合方法就都会用了。
方法如下:
方法名称 |
|
public V put(K key, V value) |
添加元素 |
ublic int size() |
获取集合的大小 |
public void clear() |
清空集合 |
public boolean isEmpty() |
判断集合是否为空,为空返回true,反之为false |
public V get(Object key) |
根捃键获取对应值 |
public V remove(Object key) |
根据键删除整个元素 |
public boolean containsKey(Object key) |
判断是否包含某个键 |
public boolean containsvalue(object value) |
判断是否包含某个值 |
public Set<K> keySet() |
获取全部键的集合 |
public Collection‹V> values() |
获取Map集合的全部值 |
使用代码演示一下功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| public class MapTest2 { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); map.put("手表", 100); map.put("手表", 220); map.put("手机", 2); map.put("Java", 2); map.put(null, null); System.out.println(map);
System.out.println(map.size());
System.out.println(map.isEmpty());
int v1 = map.get("手表"); System.out.println(v1); System.out.println(map.get("手机")); System.out.println(map.get("张三"));
System.out.println(map.remove("手表")); System.out.println(map);
System.out.println(map.containsKey("手表")); System.out.println(map.containsKey("手机")); System.out.println(map.containsKey("java")); System.out.println(map.containsKey("Java"));
System.out.println(map.containsValue(2)); System.out.println(map.containsValue("2"));
Set<String> keys = map.keySet(); System.out.println(keys);
Collection<Integer> values = map.values(); System.out.println(values);
Map<String, Integer> map1 = new HashMap<>(); map1.put("java1", 10); map1.put("java2", 20); Map<String, Integer> map2 = new HashMap<>(); map2.put("java3", 10); map2.put("java2", 222); map1.putAll(map2); System.out.println(map1); System.out.println(map2); } }
|
Map集合遍历方式1
Map集合一共有三种遍历方式,我们先来学习第一种,他需要用到下面的两个方法。
Map集合的遍历方式一:键找值,先获取Map集合全部的键,再通过遍历键来找值。
需要用到Map的如下方法:
方法名称 |
说明 |
public Set‹K> keySet() |
获取所有键的集合 |
public V get (Object key) |
根据键获取其对应的值 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public class MapTest1 { public static void main(String[] args) { Map<String, Double> map = new HashMap<>(); map.put("Josh", 162.5); map.put("Josh", 169.8); map.put("Alex", 165.8); map.put("Ben", 169.5); map.put("Philips", 183.6); System.out.println(map);
Set<String> keys = map.keySet(); for (String key : keys) { double value = map.get(key); System.out.println(key + "=====>" + value); } } }
|
Map集合遍历方式2
接下来我们学习Map集合的第二种遍历方式,这种遍历方式更加符合面向对象的思维。
前面介绍过,Map集合是用来存储键值对的,而每一个键值对实际上是一个Entry对象。
这里Map集合的第二种方式,是直接获取每一个Entry对象,把Entry存储扫Set集合中去,再通过Entry对象获取键和值。
Map提供的方法 |
说明 |
Set<Map.Entry <K, V>> entrySet() |
获取所有 “键值对”的集合 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
public class MapTest2 { public static void main(String[] args) { Map<String, Double> map = new HashMap<>(); map.put("Josh", 169.8); map.put("Alex", 165.8); map.put("Ben", 169.5); map.put("Philips", 183.6); System.out.println(map); Set<Map.Entry<String, Double>> entries = map.entrySet(); for (Map.Entry<String, Double> entry : entries) { String key = entry.getKey(); double value = entry.getValue(); System.out.println(key + "---->" + value); } } }
|
Map集合遍历方式3
Map集合的第三种遍历方式,需要用到下面的一个方法forEach,而这个方法是JDK8版本以后才有的。调用起来非常简单,最好是结合的lambda表达式一起使用。
JDK1.8开始之后的新技术(非常的简单):Lambda
•需要用到Map的如下方法
方法名称 |
说明 |
default void forEach (BiConsumer‹? super K, ? super V› action) |
结合lambda遍历Map集合 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public class MapTest3 { public static void main(String[] args) { Map<String, Double> map = new HashMap<>(); map.put("Josh", 169.8); map.put("Alex", 165.8); map.put("Ben", 169.5); map.put("Philips", 183.6); System.out.println(map);
map.forEach(new BiConsumer<String, Double>() { @Override public void accept(String k, Double v) { System.out.println(k + "---->" + v); } }); map.forEach(( k, v) -> { System.out.println(k + "---->" + v); }); } }
|
Map集合案例
学习完Map集合的基本用法之后,接下来我们做一个综合案例,将Map集合运用一下。
需求:Map集合的案例-统计投票人数
•某个班级80名学生,现在需要组织秋游活动,班长提供了四个景点依次是(A、B、C、D), 每个学生只能选择一个景点,请统计出最终哪个景点想去的人数最多。
先分析需求,再考虑怎么用代码实现
- 首先可以将80个学生选择的景点放到一个集合中去(也就是说,集合中的元素是80个任意的ABCD元素)
- 准备一个Map集合用来存储景点,以及景点被选择的次数
- 遍历80个学生选择景点的集合,得到每一个景点,判断Map集合中是否包含该景点
- 如果不包含,则存储”景点=1”
- 如果包含,则存获取该景点原先的值,再存储”景点=原来的值+1”; 此时新值会覆盖旧值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
public class MapDemo4 { public static void main(String[] args) { List<String> data = new ArrayList<>(); String[] selects = {"A", "B", "C", "D"}; Random r = new Random(); for (int i = 1; i <= 80; i++) { int index = r.nextInt(4); data.add(selects[index]); } System.out.println(data);
Map<String, Integer> result = new HashMap<>();
for (String s : data) { if(result.containsKey(s)){ result.put(s, result.get(s) + 1); }else { result.put(s, 1); } } System.out.println(result); } }
|
链接
封面图来源:https://www.pixiv.net/artworks/104735944