类集、Collection集合、Map
本文介绍Java的类集:Collection、Map、Iterator
一、类集设置的目的
普通对象数组的最大问题在于数组中的元素个数固定,不能动态扩充大小,所以早期通过链表来实现动态对象数组,这样非常复杂。Java为了方便用户操作各个数据结构,引入了类集的概念,可以把类集成为java对数据结构的实现。
类集中最大的几个操作接口:Collection、Map、Iterator
所有类集操作的接口或类都在java.util包中。
Java类集结构图:
二、Collection接口
Collection 中的子接口为List、Set
List 和Set 公用的方法:
public boolean add(E e) : 把给定的对象添加到当前集合中 。
public void clear() :清空集合中所有的元素。
public boolean remove(E e) : 把给定的对象在当前集合中删除。
public boolean contains(E e) : 判断当前集合中是否包含给定的对象。
public boolean isEmpty() : 判断当前集合是否为空。
public int size() : 返回集合中元素的个数。
public Object[] toArray() : 把集合中的元素,存储到数组中。
1.List接口
常用的实现类有:ArrayList、Vector、LinkedList
特点:
- 存取有序,例如存元素的顺序是4,2,1,那么元素在集合中就是按照4,2,1的顺序存储的。
- 是一个带有索引的集合(与数组索引相同)。
- 可以有重复的元素,可以通过元素的equals 方法来比较是否为重复的元素。
常用方法
public void add(int index, E element) : 将指定的元素,添加到该集合中的指定位置上。
public E get(int index) :返回集合中指定位置的元素。
public E remove(int index) : 移除列表中指定位置的元素, 返回的是被移除的元素。
public E set(int index, E element) :用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
1.1 ArrayList
ArrayList 的构造函数什么参数都不传递是,默认长度为0,当添加第一个元素进去后,长度变为10,已满还要添加,扩容为原来的1.5倍。
优点:查询遍历顺序快
缺点:增删慢
1.2 LinkedList
使用了大量首尾操作的方法,可以作为堆栈和队列的就够使用。
public void addFirst(E e) :将指定元素插入此列表的开头。
public void addLast(E e) :将指定元素添加到此列表的结尾。
public E getFirst() :返回此列表的第一个元素。
public E getLast() :返回此列表的最后一个元素。
public E removeFirst() :移除并返回此列表的第一个元素。
public E removeLast() :移除并返回此列表的最后一个元素。
public E pop() :从此列表所表示的堆栈处弹出一个元素。
public void push(E e) :将元素推入此列表所表示的堆栈。
public boolean isEmpty() :如果列表不包含元素,则返回true。
代码如下(示例):
LinkedList<Integer> data = new LinkedList<>();
data.addFirst(123);
data.addFirst(234);
Integer i = data.removeFirst();
//压栈
data.push(100);
data.push(200);
//弹栈
Integer i = data.pop();
2.Iterator迭代器
Iterator主要用于迭代访问操作,可以遍历Collection集合中存入的元素
主要方法有:
public Iterator iterator() : 获取集合对应的迭代器,用来遍历集合中的元素的。
public E next() :返回迭代的下一个元素。
public boolean hasNext() :如果仍有元素可以迭代,则返回 true。
代码如下(示例):
ArrayList<Integer> data = new ArrayList<>();
data.add(1);
data.add(2);
data.add(3);
data.add(4);
for(int i=0;i<data.size();i++){
System.out.println(data[i]);
}
// 取数据
Iterator<Integer> iterator = data.iterator();
while(iterator.hasNext()){ //判断
//指针向后移动并取出数据
Itegar i = iterator.next();
}
// 删除数据
iterator.next();
Iterator.remove();
3.for each(增强for循环)
它用于遍历Collection和数组。通常只进行遍历元素,不要在遍历的过程中对集合元素进行增删操作。
代码如下(示例):
int[] arr = {6,5,4,2,1};
for(int data:arr){
// 遍历arr数组,data代表遍历到的每一个元素
System.out.println(data);
}
ArrayList<String> data = new ArrayList<>();
data.add("123")
data.add("234")
for(String s:data){
// 遍历ArrayList,s代表集合中的每一个元素
System.out.println(s);
}
4.Set接口
与List接口不同的是:Set 接口中元素无序(存取顺序不一致),并且都会以某种规则保证存入的元素不出现重复。
常用的实现类有:HashSet、LinkedHashSet
Set取元素的方法可以用:迭代器、增强for
4.1 HashSet 集合
它所存储的元素是不可重复的,并且所有元素都是无序的(存取顺序不一致)。HashSet 底层的实现是HashMap支持的
HashSet 是根据对象的哈希值来确定元素在集合中的存取位置,因此具有良好的存取和查找性能
保证元素唯一性的方式依赖于:hashCode、equals方法。
代码如下(示例):
//创建 Set集合
HashSet<String> set = new HashSet<String>();
//添加元素
set.add(new String("123"));
set.add("123");
set.add("123");
set.add("321");
//遍历
for (String name : set)
{
System.out.println(name);
}
输出结果
123
321
4.2 HashSet 存储数据的结构(哈希表)
在JDK1.8之前,哈希表底层采用数组+链表 的结构实现。使用链表来处理冲突,同一hash值的元素都存储在同一个链表里。但是当一个桶(链表)中的元素较多时,即hash值相等的元素较多时,通过key值一次查找的效率较低。
在JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度大于8时,链表换为红黑树存储。
JDK1.8引入红黑树大程度优化了HashMap的性能,保证HashSet集合元素的唯一,其实就是根据存入对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。
4.3 TreeSet
数据翻入后有序排列,在存放自己写的类时要实现Comparable接口(见4.2和4.3节)
存放String 代码如下(示例):
Set<String> all = new TreeSet<String>(); // 实例化Set接口对象\
all.add("D");
all.add("X");
all.add("A");
结果:
[A, D, X]
使用TreeSet 集合添加几个Person 类:
import java.util.Objects;
// 定义Person 类
public class Person{
private String name;
private int age;
public Person() { }
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "姓名:" + this.name + ",年龄:" + this.age;
}
}
import java.util.TreeSet;
public class TreeSetPersonDemo01 {
public static void main(String[] args) {
Set<Person> all = new TreeSet<Person>();
all.add(new Person("张三", 10));
all.add(new Person("李四", 10));
all.add(new Person("王五", 11));
all.add(new Person("赵六", 12));
all.add(new Person("孙七", 13));
System.out.println(all);
}
}
执行如上代码,程序报错ClassCastException:Person类不能向Comparable接口转型
如果要使用TreeSet 存放Person 对象,必须在Person 类中实现Comparable 接口
public class Person implements Comparable<Person>{
...
@Override
public int compareTo(Person per) {
if(this.age > per.age){
return 1;
}
else if(this.age < per.age){
return -1;
}
else{
return 0;
}
}
}
程序运行结果如下:
[姓名:张三,年龄:10, 姓名:王五,年龄:11, 姓名:赵六,年龄:12, 姓名:孙七,年龄:13]
从以上的结果中可以发现,李四没有了。因为李四的年龄和张三的年龄是一样的,所以会被认为是同一个对象。则 此时必须修改 Person 类,如果假设年龄相等的话,按字符串进行排序。
public class Person implements Comparable<Person>{
...
@Override
public int compareTo(Person per) {
if(this.age > per.age){
return 1;
}
else if(this.age < per.age){
return -1;
}
else{
return this.name.compareTo(per.name);
}
}
}
此时,可以发现李四出现了,如果加入了同一个人的信息的话,则会认为是重复元素,所以无法继续加入。
4.4 使用HashSet 区分自定义类的重复元素
改用HashSet 存储Person 对象
public class HashSetPersonDemo01 {
public static void main(String[] args) {
Set<Person> all = new HashSet<Person>();
all.add(new Person("张三", 10));
all.add(new Person("李四", 10));
all.add(new Person("李四", 10));
all.add(new Person("王五", 11));
all.add(new Person("赵六", 12));
all.add(new Person("孙七", 13));
System.out.println(all);
}
}
结果为:
[姓名:王五,年龄:11, 姓名:李四,年龄:10, 姓名:张三,年龄:10, 姓名:孙七,年龄:13, 姓名:李四,年龄:10, 姓名:赵六,年龄:12]
此时发现,并没有去掉所谓的重复元素,也就是说之前的操作并不是真正的重复元素的判断,而是通过 Comparable 接口间接完成的。
如果要想判断两个对象是否相等,则必须使用 Object 类中的 equals()方法。
从最正规的来讲,如果要想判断两个对象是否相等,则有两种方法可以完成: ·
- 第一种判断两个对象的编码是否一致,这个方法需要通过 hashCode()完成,即:每个对象有唯一的编码 ·
- 还需要进一步验证对象中的每个属性是否相等,需要通过 equals()完成。
所以此时需要覆写 Object 类中的 hashCode()方法,此方法表示一个唯一的编码,一般是通过公式计算出来的。
public class Person{
...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return getAge() == person.getAge() &&
getName().equals(person.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());
}
}
运行结果
[姓名:李四,年龄:10, 姓名:孙七,年龄:13, 姓名:王五,年龄:11, 姓名:赵六,年龄:12, 姓名:张三,年龄:10]
发现,此时已经不存在重复元素了,所以如果要想去掉重复元素需要依靠hashCode()和 equals()
4.5 总结
关于 TreeSet 的排序实现:如果是集合中对象是自定义的或者说其他系统定义的类没有实现 Comparable 接口,则不能实现 TreeSet 的排序,会报类型转换(转向 Comparable 接口)错误。 也就是要添加到 TreeSet 集合中的对象的类型必须实现了 Comparable 接口。
TreeSet 的集合因为借用了 Comparable 接口,同时可以去除重复值,而 HashSet 虽然是 Set 接口子类,但是对于没有复写 Object 的 equals 和 hashCode 方法的对象,加入了 HashSet 集合中也是不能去掉重复值的。
三、 集合工具类Collections
部分方法如下:
public static boolean addAll (Collection c, T… elements) :往集合中添加一些元素。
public static void shuffle (List<?> list) 打乱顺序 :打乱集合顺序。
public static void sort (List list) :将集合中元素按照默认规则排序。
public static void sort (List list,Comparator<? super T> ) :将集合中元素按照指定规则排序。
代码如下(示例):
ArrayList<Integer> list = new ArrayList<>();
// 原来写法
// list.add(12);
// list.add(44);
// 采用工具类
Collections.addAll(list,12,44,1,2);
System.out.println(list);
// 排序方法
Collections.sort(list);
System.out.println(list);
结果:
[12, 44, 1, 2]
[1, 2, 12, 44]
经过sort() 的排序采用默认的顺序,如果要指定顺序需要用到public static void sort (List list,Comparator<? super T> ) 中的Comparator
四、Comparator 比较器
存储字符串类型的比较(示例):
public class CollectionsDemo2 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法
Collections.sort(list);
System.out.println(list);
}
}
结果:
[aba, cba, nba, sba]
1. 默认排序规则的定义
在JAVA中提供了两种实现两个对象之间比较大小的方式,一种是比较死板的采用java.lang.Comparable接口去实现,一种是灵活的当我需要做排序的时候在去选择
的 java.util.Comparator 接口完成
我们采用的 public static void sort(List list) 这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型上如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
}
String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的了,那么这个时候我们可以使用
public static void sort(List list,Comparator<? super T> )
方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:
public int compare(String o1, String o2) :比较其两个参数的顺序。
两个对象比较的结果有三种:大于,等于,小于。
如果要按照升序排序,
则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数)
如果要按照降序排序
则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)
操作如下:
public class CollectionsDemo3 {
public static void main(String[] args)
{
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法 按照第一个单词的降序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.charAt(0) - o1.charAt(0);
}
});
System.out.println(list);
}
}
结果:
[sba, nba, cba, aba]
2. 简述Comparable和Comparator两个接口的区别
Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。只能在类中实现 compareTo() 一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无序指定比较器。
Comparator:强行对某个对象进行整体排序。可以将Comparetor 传递给sort 方法(如Collections.sort 或Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用 Comparator 来控制某些数据结构(如有序set 或有序映射)的顺序,或者为那些没有自然顺序的对象 collection 提供排序。
创建一个学生类,存储到ArrayList集合中完成指定排序操作。实例如下:
// Student 初始类
public class Student{
private String name;
private int age; public Student() { }
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name)
{
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
// 测试类
public class Demo {
public static void main(String[] args) {
// 创建四个学生对象 存储到集合中
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("rose",18));
list.add(new Student("jack",16));
list.add(new Student("abc",16));
list.add(new Student("ace",17));
list.add(new Student("mark",16));
/*让学生 按照年龄排序 升序 */
// Collections.sort(list);
//要求 该list中元素类型 必须实现比较器Comparable接口
for (Student student : list) {
System.out.println(student);
}
}
}
发现,当我们调用Collections.sort()方法的时候 程序报错了。
原因:如果想要集合中的元素完成排序,那么必须要实现比较器Comparable接口。
于是我们就完成了Student类的一个实现,如下:
public class Student implements Comparable<Student>{
....
@Override
public int compareTo(Student o) {
return this.age-o.age;//升序
}
}
再次测试,代码就OK 了效果如下:
Student{name=‘jack’, age=16}
Student{name=‘abc’, age=16}
Student{name=‘mark’, age=16}
Student{name=‘ace’, age=17}
Student{name=‘rose’, age=18}
3. 扩展
如果在使用的时候,想要独立的定义规则去使用可以采用 Collections.sort(List list,Comparator c) 方法,自己定义规则:
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
//以学生的年龄降序
return o2.getAge()-o1.getAge();
}
});
结果:
Student{name=‘rose’, age=18}
Student{name=‘ace’, age=17}
Student{name=‘jack’, age=16}
Student{name=‘abc’, age=16}
Student{name=‘mark’, age=16}
如果想要规则更多一些,参考下面代码:
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 年龄降序
int result = o2.getAge()-o1.getAge();
//年龄降序
if(result==0){//第一个规则判断完了 下一个规则 姓名的首字母 升序
result = o1.getName().charAt(0)-o2.getName().charAt(0);
}
return result;
}
});
结果:
Student{name=‘rose’, age=18}
Student{name=‘ace’, age=17}
Student{name=‘abc’, age=16}
Student{name=‘jack’, age=16}
Student{name=‘mark’, age=16}
五、Map
Collection中,每次操作的都是一个对象,如果要操作一对对象,则需使用Map。Map接口中所有的内容都按照 key->value 的形式保存
自己定义的对象最好不要存到键的位置,如果存了,尽量不要改变,否则哈希码会乱
1. HashMap
无序排放
向集合中添加内容,范例如下:
HashMap<Integer,String> map = new HashMap<Integer,String>();
map.put(1,"张三A");
map.put(1,"张三B"); //新内容替换旧内容
map.put(2,"李四");
map.put(3,"王五");
// 根据指定的key 找到value,如果没有找到,则返回 null,找到了返回具体的内容
String val= map.get(6);
System.out.println(val);
得到全部的key 或 value,范例如下:
HashMap<Integer,String> map = new HashMap<Integer,String>();
map.put(1,"张三A");
map.put(1,"张三B"); //新内容替换旧内容
map.put(2,"李四");
map.put(3,"王五");
Set<Integer> set = map.keySet(); //得到全部的key
Collection<String> value = map.values(); //得到全部的value
Iterator<Integer> iter1 = set.iterator();
Iterator<String> iter2 = value.iterator();
System.out.println("全部的key:");
while(iter1.hasNext()){
System.out.print(iter1.next()+" ");
}
System.out.print("\n全部的Value:");
while(iter2.hasNext()){
System.out.print(iter2.next()+" ");
}
结果:
全部的key:
1 2 3
全部的Value:张三B 李四 王五
循环输出Map中的全部内容
Map<String, String> map = new HashMap<String, String>();
map.put("ZS", "张三");
map.put("LS", "李四");
map.put("WW", "王五");
map.put("ZL", "赵六");
map.put("SQ", "孙七");
Set<String> set = map.keySet(); // 得到全部的key
Iterator<String> iter = set.iterator();
while (iter.hasNext()) {
String i = iter.next(); // 得到key
System.out.println(i + " --:> " + map.get(i));
}
2.TreeMap (较少使用)
按照 key 进行排序,另外,可以中的内容可以为任一对象,但要求对象所在的类必须实现Comparable接口。
Map<String, String> map = new TreeMap<String, String>();
map.put("ZS", "张三");
map.put("LS", "李四");
map.put("WW", "王五");
map.put("ZL", "赵六");
map.put("SQ", "孙七");
Set<String> set = map.keySet(); // 得到全部的key
Iterator<String> iter = set.iterator();
while (iter.hasNext()) {
String i = iter.next(); // 得到key
System.out.println(i + " --:> " + map.get(i));
}
3.Map集合的输出
在 Collection 接口中,可以使用 iterator() 方法为Iterator 接口实例化,并进行输出操作,但是在 Map 接口中并没有此方法的定义,不能直接用 Iterator 进行输出。
Map接口中存放的每一个内容都是一对值,而使用 Iterator 接口输出时,每次取出的都是一个完整的对象。如果此时非要用 Iterator 进行输出,则可按照如下步骤进行:
- 使用Map 接口中的entrySet() 方法将Map 接口的内容变为Set 集合
- 可以使用Set 接口中定义的 iterator() 方法为Iterator 接口进行实例化
- 之后使用Iterator 接口进行迭代输出,每一次迭代都可以取得一个Map.Entry 的实例
- 通过Map.Entry 进行key 和 value的分离
Map.Entry 接口中最常用的方法:
K getKey() 得到key
V getValue() 得到value
HashMap<String,String> map = new HashMap<>();
map.put("ZS","张三");
map.put("LS","李四");
map.put("WW","王五");
map.put("ZL","赵六");
Set<Map.Entry<String,String>> set = map.entrySet(); //变为Set 实例
Iterator<Map.Entry<String,String>> iter = set.iterator();
while(iter.hasNext()){
Map.Entry<String,String> me = iter.next();
System.out.println(me.getKey()+"->"+me.getValue());
}
以上的代码一定要记住,Map 集合中每一个元素都是 Map.Entry 的实例,只有通过 Map.Entry 才能进行 key 和 value 的分离操作。
除了以上的做法之外,在 JDK 1.5 之后也可以使用 foreach 完成同样的输出,只是这样的操作基本上不使用。
HashMap<String,String> map = new HashMap<>();
map.put("ZS","张三");
map.put("LS","李四");
map.put("WW","王五");
map.put("ZL","赵六");
for (Map.Entry<String, String> me : map.entrySet()) {
System.out.println(me.getKey() + " --> " + me.getValue());
}
4.HashMap、Hashtable、ConcurrentHashMap区别:
HashMap线程不安全
HashTable 线程安全,但是效率低,线程排队在整个集合外
ConcurrentHashMap 采用分段锁机制,保证线程安全,效率比较高。 操作不同下标的数据可以同时访问修改,但是操作相同的下标要排队
HashMap 不保证有序,因为是根据哈系码存入的
TreeMap 在存入的后进行排序
LinkedHashMap 既存在双向链表中又存在HashMap中,可以保证存储顺序