1. 集合的简介
1.1什么是集合
集合Collection,也是一个数据容器,类似于数组,但是和数组是不一样的。集合是一个可变的容器,可以随时向集合中添加元素,也可以随时从集合中删除元素。另外,集合还提供了若干个用来操作集合中数据的方法。集合里的数据,我们称之为元素(elements);集合只能用来存储引用类型的数据,不能存储八大基本数据类型的数据。
泛型
Java SE 5.0以前,集合的元素只要是Object类型就行,那个时候任何对象都可以存放在集合内,但是从集合中获取对象后,需要进行正确的强制类型转换。但是,Java SE 5.0 以后,可以使用新特性”泛型”,用来指定要存放在集合中的对象类型。避免了强制转换的麻烦。
1.2 集合与数组
- 数组是定长的容器,一旦实例化完成,长度不能改变。集合是变长的,可以随时的进行增删操作。
- 数组中可以存储基本数据类型和引用数据类型的元素,集合中只能存储引用数据类型的元素。
- 数组的操作比较单一,只能通过下标进行访问。集合中提供了若干个方便对元素进行操作的方法。
小贴士: 在存储引用类型时,集合与数组,存储的其实都是对象的地址。
2.Collection接口
2.1 简介
Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义了他们三个子接口的共同方法。既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。作为父接口,其子类集合的对象,存储元素的特点,可能是无序的,也可能是有序的,因此在父接口中并没有定义通过下标获取元素的方法功能。
2.2 常用方法
方法如下:
- E add(E e, 添加元素。
- boolean isEmpty(), 判断集合是否为空
- int size() 集合中元素的个数
- String toString() 将集合元素全都转化成字符串形式。
- addAll(Collection c) 在尾部添加一个集合里的所有元素。
- boolean contain(Object o) 判断集合中是否包含元素o
- boolean containAll(Collection c) 判断集合是否含有所有c集合中的元素。
- boolean equal(Collection c) 判断与集合c是否相等
- boolean remove(Object o) 删除集合中的第一个元素是o的元素
- boolean removeAll(Collection c) 删除集合中存在于集合c中的元素
- boolean retainAll(Collection<?> c) 保留集合中存在于集合c中的元素
- void clear() 清空集合
public class CollectionDemo {public static void main(String[] args) {//使用多态的向上造型创建一个子类对象Collection<String> c1 = new ArrayList<String>();//1. E add(E e) 向集合中添加元素c1.add("A");c1.add("B");c1.add("C");//2. boolean isEmpty()boolean isEmpty = c1.isEmpty();System.out.println("isEmpty = " + isEmpty);//3. int size() 返回的是集合元素的个数。System.out.println("size: " + c1.size());//4. String toStringCollection<String> c2 = new ArrayList<>();c2.add("B");c2.add("C");c2.add("D");System.out.println("c1"+c1);//5.addAll(Collection c)c1.addAll(c2);System.out.println("c1 = " + c1);//6. boolean contains(Object o) //查看是否包含Bboolean flag = c1.contains("B");System.out.println("c1是否包含B: "+flag);//7. boolean containsAll(Collection e)boolean flag1 = c1.containsAll(c2);System.out.println("c1集合是否包含c2集合"+flag1);//8. boolean equals(Collection c)//添加元素 “A”"B""C""B""D""D"// 测试:创建一个新的集合c3判断两个集合是否相同Collection<String> c3 = new ArrayList<>();c3.add("A");c3.add("B");c3.add("C");c3.add("B");c3.add("D");c3.add("D");System.out.println("c1和c3相同吗?"+c1.equals(c3));/*** 9. boolean remove(Object o)* 只移除一个元素。* 移除c1集合里的元素D,并查看剩下的元素*/boolean flag2 = c1.remove("D");System.out.println("c1移除D后" + flag2);System.out.println("c1"+c1);/*** 10.boolean removeAll(Collection<?> c)//移除集合中 另一个c集合中存在元素 的所有元素。* 新建一个集合c4,添加元素B\C* 测试c1移除子集c4*/Collection<String> c4 = new ArrayList<>();c4.add("B");c4.add("C");boolean flag3 = c1.removeAll(c4);System.out.println("c1移除c4后 " + flag3);System.out.println("c1"+c1);/*** 11. boolean retainAll(Collection<?> c)* 测试:向c1里添加"E""F""G",然后保留子集c5,c5里的元素有"A""E"。**/c1.add("E");c1.add("F");c1.add("G");Collection<String> c5 = new ArrayList<>();c5.add("A");c5.add("E");boolean flag4 = c1.retainAll(c5);System.out.println("c1保留元素c5:"+flag4);System.out.println("c1"+c1);/*** 12 void clear()* 清空c1*/c1.clear();System.out.println("c1清空后:"+c1);System.out.println("c1.size"+c1.size());}
}
2.3 集合的迭代
在进行集合的遍历的时候,方式其实很多。但是,基本上所有的遍历,都与 Iterable 接口有关。这个接 口提供了对集合集合进行迭代的方法。只有实现了这个接口,才可以使用增强for循环进行遍历。也就是说,增强for循环的底层逻辑就是迭代器。
集合的迭代:
1. 集合的迭代,就是指集合的遍历操作。
2. 几乎所有的集合子类型都实现了迭代接口Iterable
3. 迭代接口提供了多个方法,用于迭代
- boolean hasNext(): 用于判断集合中是否有下一个元素。
可以形象的认为有指针,指针的最初位置,在第一个元素的前面。
- E next(): 用于返回指针指向的那个元素,然后指针向后移动,为下一次判断做准备。
- E remove(): 用于删除指针指向的那个元素。
注意,在使用迭代器时,不可以调用集合自己的remove方法。
演示代码:
public class CollectionDemo02 {public static void main(String[] args) {Collection<Integer> c1 = new ArrayList<>();c1.add(1);c1.add(2);c1.add(3);c1.add(4);c1.add(5);//使用增强for循环遍历(foreach):底层原理就是迭代器。for(Integer i : c1){System.out.println(i+" ");}/*** 使用迭代器iterator进行遍历*///第一步: 获取要遍历的集合对象的迭代器对象Iterator<Integer> it = c1.iterator();//第二步:判断是否有下一个元素。也就是循环条件while(it.hasNext()){//第三步 : 取出元素,再循环内。Integer a = it.next();System.out.println("a: "+a);}}
}
2.4迭代器的详细介绍与Debug调试
Debug调试:
- IDE(idea,eclipse)这样的开发工具应该都有debug功能。该功能是用来调试代码,捋清运行逻辑,找到发生异常的地方。
- 如果想要使用debug功能,需要在可能出现异常或者你想要看的地方打一个断点.
- 断点的作用: 就是在debug运行到这一地方时,会进行程序的阻塞。
- 然后可以使用相应的快捷键或者按钮,来进行一步一步的跟踪。
迭代器的详细介绍:
迭代器的三个成员变量
cursor:记录下一个元素要返回的下标
lastRet:记录上一次返回的元素的下标
exceptedModCount 预计的修改次数。默认为集合的长度,与modCount的值有关
注意:集合在调用add(E e)方法添加元素时,modCount++。
部分源码如下:
private class Itr implements Iterator<E> {int cursor; // index of next element to returnint lastRet = -1; // index of last element returned; -1 if no suchint expectedModCount = modCount;Itr() {}public boolean hasNext() {return cursor != size;}@SuppressWarnings("unchecked")public E next() {checkForComodification();int i = cursor;if (i >= size)throw new NoSuchElementException();Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length)throw new ConcurrentModificationException();cursor = i + 1;return (E) elementData[lastRet = i];}public void remove() {if (lastRet < 0)throw new IllegalStateException();checkForComodification();try {ArrayList.this.remove(lastRet);cursor = lastRet;lastRet = -1;expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}
hasNext() 方法 return cursor!=size : 当光标的值等于集合长度时,就会返回false值,也就代表着没有下一个要返回的元素了。
E next()方法:
首先执行checkForComodification()方法
源码如下:
final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}
如果modCount 与expectedModCount值不相等时,则会抛出并发修改异常。 此代码主要防止调用非迭代器方法而造成迭代器故障。这是因为迭代器依赖于集合的内部状态来正确地进行遍历,而外部修改可能会破坏这个状态。再删除时,要调用迭代器的remove方法,而不是集合的remove方法。
看代码注释部分:
public E next() {//检查expectedModCount与ModCount方法checkForComodification();//将cursor也就是光标所指的值赋给i,方便后面进行修改赋值。int i = cursor;//越界判断if (i >= size)throw new NoSuchElementException();// 将集合各元素赋值给数组Object[] elementData = ArrayList.this.elementData;//如果光标在数组索引之外,会发生异常if (i >= elementData.length)throw new ConcurrentModificationException();//异常判断未发生,执行下面代码,将光标指向下一个元素cursor = i + 1;// 将之前光标所指的值也就是现在指针(用i给指针赋值为刚开始光标的值)所指的值返回。return (E) elementData[lastRet = i];}
文字解释
1.检查expectedModCount 和 modCount 是否一样, 如果不一致,就抛异常。
2.光标的值先复制给lastRet。 相当于指针后移。光标的值+1,为下一次的hasNext()做准备工作。
新的lastRet的值就是指针指向的元素,也就是刚刚取出来的元素。
remove()方法
直接上源代码看注释 部分
public void remove() {//如果值错误直接抛异常if (lastRet < 0)throw new IllegalStateException();//检查expectedModCount与ModCount的值是否相同。源码上面写过了checkForComodification();try {//调用了ArrayList里的remove方法。源码在下面部分。ArrayList.this.remove(lastRet);将光标的值后移,也就是-1操作,等于lastRetcursor = lastRet;将lastRet指针赋值为-1,因为已经没有上一个要返回的元素了。需要等重新赋值lastRet = -1;//两个值相等。也是迭代器remove方法独有的。expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}
ArrayList.this.remove(lastRet);
调用了ArrayList里的remove方法。
ArrayList的remove源码如下:
public E remove(int index) {源码在下面,用于判断是否越界rangeCheck(index);//modCount++操作。modCount++;//存下将要删除的元素E oldValue = elementData(index);// 下面数组赋值需要赋值的长度int numMoved = size - index - 1;if (numMoved > 0)//调用系统的数组复制的方法,删除某数组System.arraycopy(elementData, index+1, elementData, index,numMoved);//将最后的数组值赋值为空。因为在复制的时候他已经前移了。elementData[--size] = null; // clear to let GC do its work//返回删掉的元素return oldValue;}
上部分rangeCheck()源码如下
越界判断的源码
private void rangeCheck(int index) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}
文字总结:
1. 检查expectedModCount 和 modCount 是否一样, 如果不一致,就抛异常。
2. 调用ArrayList的E remove(int index)方法做真正的删除元素操作。
而E remove(int index)里面调用了system.arraycopy()方法,
从指定的index+1处,向前移动一位,以此形式做删除工作。
ndex的值是lastRet赋值的。
因为后续的元素向前移动了。因此cursor也要向前移动一次,即cursor=lastRet操作。
如果不向前移动,会漏掉元素。index+1这个位置上的元素,就会被漏掉。
最重要的是,remove(int index)里面进行了modCount++操作。
但是迭代器的remove里,实现了expectedModCount = ModCount操作。
3 List子接口
3.1 简介
List 是一个元素有序、且可重复的集合,集合中的每个元素都有其对应的顺序索引,从0开始
List 允许使用重复元素,可以通过索引来访问指定位置的集合元素。
List 默认按元素的添加顺序设置元素的索引。
List 集合里添加了一些根据索引来操作集合元素的方法
3.2 ArrayList 与LinkedList
这两个类都是List接口的实现类(子类)。两者在实现上的底层原理对比
ArrayList是实现了基于动态数组的数据结构,对象存储在连续的位置上
LinkedList基于双链表的数据结构,链表中的每个节点都包含了前一个和后一个元素的引用。
在大量数据或者操作繁琐的情况下的比较:
对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。
对于插入和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
两个古老的前辈类型:
-
Vector:是一个古老的集合, JDK1.0版本时候出现,现在已经被ArrayList和LinkedList替代。
-
Stack:是一个古老的集合, JDK1.0版本时候出现,模拟栈结构存储数据。
3.3 常用方法
- 添加元素
- boolean add(E e) : 向列表末尾添加指定的元素
void add(int index, E element):在列表的指定位置添加元素
boolean addAll(Collection c ):将集合c中的所有元素添加到列表的结尾
boolean addAll(int index, Collection c):将集合c中的所有元素添加到列表的指定位置
- 获取元素 E get(int index):返回列表指定位置的元素
- 查找元素
- int indexof(Object obj) 返回列表中指定元素第一次出现的位置,如果没有该元素,返回-1
- int lastindexof(Object obj)返回列表中指定元素最后一次出现的位置,如果没有该元素,返回-1
- 移除元素 E remove(int index): 移除集合中指定位置的元素,返回被移除掉的元素对象
- 修改元素 E set(int index,E element): 用指定元素替换指定位置上的元素,返回被替换出来的元素对象
- 截取元素 List subList(int fromindex , int toindex):截取子集,返回集合中指定的fromIndex 到 toIndex之间部分视图,包前不包后
测试代码如下:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;/*** List接口的特点:* 1.元素有序,并且可以重复。* 2. 两个实现类:ArrayList和LinkedList* -ArrayList: 动态数组+各种方法* -LinkedList: 双向链表+各种方法* 3. 两个实现类的效率:(元素特别多的情况下)* - 对于随机访问来说,ArrayList要高于LinkedList* - 对于频繁地插入和删除来说, LinkedList要高于ArrayList。** 4.基于List接口,两个实现类的方法基本一致。**/public class ListDemo01 {public static void main(String[] args) {//创建一个ArrayList集合对象List<String> names = new ArrayList<>();//创建一个LinkedList集合对象List<String> adds =new LinkedList<>();//添加元素:add//boolean add(E e):添加元素,默认添加到集合尾部// void add(int index,E element): 插入元素,下标范围[0,size()]// boolean addAll(Collectionc)// boolean addAll(int index,Collection c)names.add("小明");names.add(1,"王八蛋");//添加地址。adds.add("beijing");adds.add("shanghai");//将地址集合添加到名字集合中names.addAll(adds);System.out.println(names);//getString str = names.get(2);System.out.println("str"+str);//int indexOf(Object obj)int i = names.indexOf("王八蛋");System.out.println("i "+i);names.add("小明");//int lastIndexOf(Object obj)int j = names.lastIndexOf("小明");System.out.println("lastindexof " + j);System.out.println("删除前: "+names);//E remove(index)String str2 = names.remove(3);System.out.println("被移除的元素是:"+str2);System.out.println("删除后 :"+names);//E set(int index,E element)str = names.set(1,"wbd");System.out.println(str);System.out.println(names);//List subList(int fromIndex, int toIndex)// 用于截取子集,包前不包后。List<String> list = names.subList(0,2);System.out.println("list"+list);List<Integer> list2 = new ArrayList<>();for (int k = 1; k <11 ; k++) {list2.add(k);}List<Integer> list1 = list2.subList(3,7);//对子集里的每一个元素扩大十倍for (int k = 0; k < list1.size(); k++) {list1.set(k,list1.get(k)*10);}System.out.println(list1);System.out.println(list2);}
}
4. Queue接口
4.1 简介
-
队列Queue也是Collection的一个子接口,它也是常用的数据结构,可以将队列看成特殊的线性表,队列限制对线性表的访问方式:只能从一端添加(offer)元素,从另一端取出(poll)元素。
-
队列遵循先进先出(FIFO first Input First Output)的原则
-
实现类LinkedList也实现了该接口,选择此类实现Queue的原因在于Queue经常要进行添加和删除操作,而LinkedList在这方面效率比较高。
-
其主要方法如下:
方法 | 解析 |
---|---|
boolean offer(E e) | 作用:将一个对象添加到队尾,如果添加成功返回true |
E poll() | 作用:从队首删除并返回这个元素 |
E peek() | 作用:查看队首的元素 |
Queue: 队列,也是collection接口的子接口
用来描述一种先进先出(First Input First Output)的数据存储结构
可以理解为是一 种特殊的线性表,只能从一头进入,另一头出来,对应的方法
boolean offer(E e):从队列的尾部进入
E poll(): 从队列的头部出来
E peek(): 查看头部的元素
代码演示
public class QueueDemo01 {public static void main(String[] args) {// 创建一个名字队列Queue<String> names = new LinkedList<String>();// 调用offer方法,入队names.offer("xianshen");names.offer("张三");names.offer("李四");//查看队列System.out.println(names);
// 查看队列的头部对象String name = names.peek();System.out.println(name);// 头部元素出队while(names.peek() != null) {// 如果想要移除头部元素,最后要先检查队头元素是否为空。String name1 = names.poll();System.out.println(name1);System.out.println(names);}
// while(!names.isEmpty()) {
// String name1 = names.poll();
// System.out.println(name1);
// System.out.println(names);
// }}
}
4.2 Deque
Deque是Queue的子接口,定义了所谓的”双端队列”,即从队列的两端分别可以入队(offer)和出队(poll)。同样,LinkedList实现了该接口
代码部分
public class QueueDemo02 {public static void main(String[] args) {//创建一个双端队列Deque<String> names = new LinkedList<>();//添加第一个元素names.offer("齐大");//第二个元素从队列的头部进入names.offerFirst("乔二");//第三个元素从队列的尾部进入 offer() offerLastnames.offerLast("张三");System.out.println(names);//移除元素String headName = names.poll();System.out.println("Head name: " + headName);//移除队尾String tailName = names.pollLast();System.out.println("Tail name: " + tailName);System.out.println(names);}
}
4.3 Deque模拟栈
如果将Deque限制为只能一端入队和出队,则可以实现“栈”(Stack)的数据结构,对于栈而言,入栈被称为push,出栈被称为pop。遵循先进后出(FILO)原则
练习代码如下:
import java.util.Deque;
import java.util.LinkedList;public class QueueDemo03 {public static void main(String[] args) {// 创建一个存储结构Deque<String> stack = new LinkedList<>();Deque<Integer> stack1 = new LinkedList<>();stack1.push(1);stack1.push(2);stack1.push(3);stack1.push(4);System.out.println(stack1.pop());//从队列的出口进入,对应的就是栈的出口(也是入口)进入,对应的方法push,表示将元素推进去stack.push("a");//推入到栈的底部stack.push("b");stack.push("c");stack.push("d");stack.push("e");System.out.println(stack);//出栈:while(stack.size() > 0){System.out.println("stack.peek(): " + stack.peek());String str = stack.pop();System.out.println("str: "+str);System.out.println(stack);}}
}
5. Set子接口
5.1 set集合的特点
- 1.set集合中的元素是无序的(取出顺序与存入顺序无关)
- 2.Set集合中的元素不能重复。
即不能把一个东西或者相似的的东西添加到同一个Set容器中,每次放入时都会进行判断是否存在,如果存在,就不添加。如果能放入,与我们设计初衷违背。
5.2 HashSet
地位:HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
优势:HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取和查找性能。
特点:
- 不能保证元素的排列顺序
- HashSet不是线程安全的
- 集合元素可以是null。
原理:
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值决定该对象在 HashSet 中的存储位置。 如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。
1)HashSet的简单实现
Set 接口:是Collection的子接口。
设计初衷:
用来存储不重复的元素,元素的顺序是无序的(取出的顺序和存入的顺序无关)。
(一旦存入,在存储结构里的顺序就固定了,和存入的先后顺序无关)
Set 接口里的方法都是Collection接口的方法。
HashSet: 是实现了Set接口的最最经典的子类型
底层的存储方式使用的是hash算法(哈希表)
public class SetDemo01 {public static void main(String[] args) {// 创建一个Set接口的对象Set<String> names = new HashSet<>();//存入michael,lucy,john,tomnames.add("michael");names.add("lucy");names.add("john");names.add("tom");System.out.println("names: " + names);/*** 打印一下四个元素的hash值*/System.out.println("michael:".hashCode()%16);System.out.println("tom:".hashCode()%16);System.out.println("john:".hashCode()%16);System.out.println("lucy:".hashCode()%16);//添加michael,因为数据结构中已经存在michael,所以新的michal是添加不进来的/*去重原理:先计算要添加的元素的hash值,然后确定哈希表中该值的位置上,是否存在元素,如果不存在,可以添加,如果存在,就需要调用equals进行比较。如果equals返回true,说明不能再存入了。如果equals返回false,说明可以存入。注意,每个hash值对应的位置上都维护了一个单向链表。可以进入存储hash值相同,equals不同的元素。*/names.add("michael");System.out.println(names);System.out.println(names.size());}
}
上述方法总结,定义一个Set<>接口实现HashSet子类的对象,然后调用添加方法,添加同样的名字,查看有没有真正的加进来。
2)HashSet的遍历
代码为:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;/*** hashSet的遍历*/
public class SetDemo02 {public static void main(String[] args) {Set<Integer> set = new HashSet<Integer>();set.add(10);set.add(20);set.add(30);set.add(40);set.add(50);System.out.println(set);/*1.set是无序的,因此不能使用fori遍历2.使用增强for循环遍历*/for (Integer i : set) {System.out.println(i);}/*3.使用迭代器进行遍历*/Iterator<Integer> it = set.iterator();while(it.hasNext()){Integer i = it.next();System.out.println(i);}}
}
总结:Hashset集合没有存入顺序,所以无法使用for循环来遍历。可以使用迭代器和增强for循环来遍历。
3)关于HashSet的重写
下面用代码来演示:
Person类(没有重写equals和hasCode方法)
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;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}
}
添加测试(没有重写equals和hashCode方法):
public class PersonTest {public static void main(String[] args) {Set<Person> ps = new HashSet<Person>();// 存入集合四个Person对象ps.add(new Person("micheal",18));ps.add(new Person("john",19));ps.add(new Person("tom",18));ps.add(new Person("micheal",18));System.out.println("ps: "+ps);//查找michael 18Person p1 = new Person("micheal",18);// 调用contains方法判断是否包含p1boolean b = ps.contains(p1);System.out.println("是否包含: "+b);}
}
输出结果:
由输出结果可知,set集合的添加并不符合约束条件(不能重复)。所以自己定义的类需要重写equals和hashCode方法。(重写这两个方法主要也是应用在这个地方,尤其是重写hashCode方法)
Person类(重写了equals和hashCode方法)
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;}
// 重写hashcode/*重写原则:尽量让所有的成员变量都参与运算。*/@Overridepublic int hashCode() {return name.hashCode()+age;}//重写equals方法@Overridepublic boolean equals(Object obj){if(obj == null){return false;}if(!(obj instanceof Person)){return false;}if(this == obj){return true;}Person p = (Person)obj;return name.equals(p.getName()) && age == p.getAge();}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}'+"\n";}
}
重写hashCode方法的原则,尽量让所有的成员变量都参与运算。
重写equals和hashCode方法后的测试:
public class PersonTest {public static void main(String[] args) {Set<Person> ps = new HashSet<Person>();// 存入集合四个Person对象ps.add(new Person("micheal",18));ps.add(new Person("john",19));ps.add(new Person("tom",18));ps.add(new Person("micheal",18));System.out.println("ps: "+ps);//查找michael 18Person p1 = new Person("micheal",18);// 调用contains方法判断是否包含p1boolean b = ps.contains(p1);System.out.println("是否包含: "+b);}
}
同样的代码。输出结果如下:
结果说明:重复的人没有添加进来,并且相同的名字的人的hashCode值也相同,所以包含。
4) 关于修改元素对hashSet的影响
如果对添加到Set集合之后的元素进行修改操作,它的hashcode值会改变,但是它所处的位置并没有变。所以后面添加跟修改元素修改前一样的元素也能进入,因为虽然他们所处一样的位置,但是他们的值并不一样,所以在这个位置的链表会添加进来。然后添加跟它修改后相同的元素,输入他们hashcode的值一样,但是所在位置并不相同,因为修改元素并没有发生位置的改变。
代码演示如下:
修改前的代码:
public class PersonTest02 {public static void main(String[] args) {Set<Person> set = new HashSet<>();Person p1 = new Person("小明",18);Person p2 = new Person("小八",19);Person p3 = new Person("小王",18);set.add(p1);set.add(p2);set.add(p3);// 因为我们重写了equals和hashCode方法,所以p4添加失败。Person p4 = new Person("小王",18);set.add(p4);System.out.println(set);set.add(p3);set.add(p4);System.out.println(set);}
}
结果:
修改元素后的代码:
public class PersonTest02 {public static void main(String[] args) {Set<Person> set = new HashSet<>();Person p1 = new Person("小明",18);Person p2 = new Person("小八",19);Person p3 = new Person("小王",18);set.add(p1);set.add(p2);set.add(p3);// 因为我们重写了equals和hashCode方法,所以p4添加失败。Person p4 = new Person("小王",18);set.add(p4);System.out.println(set);
// 修改p3的年龄为20岁p3.setAge(20);set.add(p3);set.add(p4);System.out.println(set);}
}
5.3 LinkedHashSet
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 集合根据元素的 hashCode 值来决定元素的存储位置,但它同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
- LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
- LinkedHashSet 不允许集合元素重复。
演示代码:
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;public class SetDemo03 {public static void main(String[] args) {Set<String> set = new LinkedHashSet();set.add("a");set.add("b");set.add("h");set.add("d");set.add("e");set.add("e");System.out.println(set);Iterator<String> iterator = set.iterator();while (iterator.hasNext()) {System.out.println(iterator.next());}}
}
输出结果演示:
可以看出输出顺序跟插入顺序完全相同。如果是hashSet的话,输出的结果是无序的。
5.4 TreeSet
TreeSet的应用:
- TreeSet 是SortedSet接口的实现类。
- TreeSet 集合的元素是有序的,即内部维护一个二叉树算法的排序方式
- TreeSet 集合里的元素也不能重复
- TreeSet默认排序是升序方式(自然排序),如果想要改变默认的排序方式,可以自定义一个比较器,传入构造器中
测试代码如下:
public class SetDeno04 {public static void main(String[] args) {Set<String> set = new TreeSet<>();set.add("A");set.add("H");set.add("B");set.add("F");set.add("D");set.add("E");set.add("G");System.out.println(set);Comparator<Integer> c1 = new Comparator<Integer>() {public int compare(Integer o1, Integer o2) {return o2 - o1;}};Set<Integer> set1 = new TreeSet<>(c1);set1.add(1);set1.add(20);set1.add(3);set1.add(40);set1.add(5);System.out.println(set1);}
}
结果如下:
顺序排列。
注意案例代码调用比较器的方法,采用匿名内部类,而自定义比较器,主要是修改里面的compareTo方法,升序的话返回值用第一个减第二个,降序就用后一个减前一个。
5.5 去重原理
1)HashSet 、LinkedHashSet
2)TreeSet
如果元素的比较规则中,两个对象的比较结果是0,则视为是同一个元素,去重。
6. List排序
6.1 Comparable接口
自定义的类,如果想要排序,不管是在数组中,还是在集合中。
要求元素必须是Comparable的子类型,因为底层需要调用Comparable的CompareTo方法。
所以,自定义的类,如果想要排序,那么必须实现Comparable接口,以及重写compareTo方法。
在上述的基础之上,如果想要重新指定排序方式,不应该修改compareTo方法里的逻辑,而是应该使用Comparator比较器接口,来定义一个新的比较规则。调用集合或者数组的相关重载方法,可以传入一个比较器的这样的方法,进行新的排序。
演示代码如下:
要求:
向TreeSet集合中加入5个员工的对象,根据员工的年龄(升序)进行排序,若年龄相同再根据工龄(降序)来排序,若工龄相同,根据薪水(降序)排序
import java.util.TreeSet;public class Staff implements Comparable<Staff>{private String name;private int age;private int workAge;private int salary;public Staff(String name, int age, int workAge, int salary) {this.name = name;this.age = age;this.workAge = workAge;this.salary = salary;}@Overridepublic int compareTo(Staff o) {if(this.age == o.age){if(this.workAge == o.workAge){return o.salary-this.salary;}return o.workAge-this.workAge;}return this.age - o.age;}@Overridepublic String toString() {return "Staff{" +"name='" + name + '\'' +", age=" + age +", workAge=" + workAge +", salary=" + salary +'}';}public static void main(String[] args) {Staff staff1 = new Staff("张三",55,20,12230);Staff staff2 = new Staff("张1",45,20,1230);Staff staff3 = new Staff("张2",45,20,12230);Staff staff4 = new Staff("张3",45,26,12230);Staff staff5 = new Staff("张4",45,26,23333);TreeSet<Staff> staffs = new TreeSet<>();staffs.add(staff1);staffs.add(staff2);staffs.add(staff3);staffs.add(staff4);staffs.add(staff5);System.out.println(staffs);}
}
输出结果:
方法二:匿名内部类重写compareTo方法
import java.util.Comparator;
import java.util.TreeSet;public class Staff2 {private String name;private int age;private int workAge;private int salary;public Staff2(String name, int age, int workAge, int salary) {this.name = name;this.age = age;this.workAge = workAge;this.salary = salary;}@Overridepublic String toString() {return "Staff2{" +"name='" + name + '\'' +", age=" + age +", workAge=" + workAge +", salary=" + salary +'}';}public static void main(String[] args) {Staff2 staff1 = new Staff2("张三",55,20,12230);Staff2 staff2 = new Staff2("张1",45,20,1230);Staff2 staff3 = new Staff2("张2",45,20,12230);Staff2 staff4 = new Staff2("张3",45,26,12230);Staff2 staff5 = new Staff2("张4",45,26,23333);Comparator<Staff2> comparator = new Comparator<Staff2>() {@Overridepublic int compare(Staff2 o1, Staff2 o2) {if(o1.age == o2.age){if(o1.workAge == o2.workAge){return o2.salary-o1.salary;}return o2.workAge-o1.workAge;}return o1.age - o2.age;}};TreeSet<Staff2> staffSet = new TreeSet<>(comparator);staffSet.add(staff1);staffSet.add(staff2);staffSet.add(staff3);staffSet.add(staff4);staffSet.add(staff5);System.out.println(staffSet);}
}
结果:
6.2 工具类提供的排序方法
Collections是集合的工具类,提供了很多便于我们操作集合的方法,其中就有用于集合排序的sort方法。作用是对指定的集合元素进行自然排序。前提元素类型必须实现Comparable接口。
集合工具类:Collections。 数组工具类: Arrays
集合工具类和数组工具类一样,都提供了很多常用的方法,来操作对象、
void sort(List<E> list): 对集合元素进行升序排序
void sort(List<E> list, Comparator<E> c): 可以使用比较器来重新定义比较规则。
演示代码如下:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;public class SortDemo02 {public static void main(String[] args) {List<Integer> list = new ArrayList<Integer>();for (int i = 0; i < 10; i++) {list.add((int) (Math.random() * 100));}System.out.println(list);/*** 利用工具类里的排序方法*/Collections.sort(list);System.out.println(list);/*** 由于集合工具类的sort(List list)是默认升序剖析。现在呢* 想要重新指定排序规则:降序。那么就需要调用另外一个方法sort(List list,Comparator)*/Comparator<Integer> c1 = new Comparator<Integer>() {public int compare(Integer o1, Integer o2) {return o2 - o1;}};Collections.sort(list,c1);System.out.println(list);}}
结果:
7.Collections工具类
7.1 Collections方法
Collections 是一个操作 Set、List 和 Map 等集合的工具类,提供了大量方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
1)排序操作
reverse(List):反转 List 中元素的顺序
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
2)查找、替换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回指定集合中指定元素的出现次数
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
代码测试:
public class CollectionsDemo {public static void main(String[] args) {List<String> names = new LinkedList<>();names.add("John");names.add("Jane");names.add("Bob");names.add("Jack");names.add("Mary");names.add("Tom");//打印集合,顺序为添加时的顺序。System.out.println(names);//1. 调用集合工具类里的反转方法,将集合元素颠倒过来Collections.reverse(names);System.out.println("reverse:"+names);// 2. 调用工具类里的洗牌(打乱)方法Collections.shuffle(names);System.out.println("打乱后的结果: "+names);// 3 交换Collections.swap(names,1,names.size()-2);System.out.println("交换后的结果: "+names);// 4. 调用max方法, 底层使用的是自然排序,找到最大的String str = Collections.max(names);System.out.println("最大的:"+str);// 调用min方法 底层使用的是自然排序,找到最小的。String str1 = Collections.min(names);System.out.println("最小的:"+str1);// 6.7 按比较器规则// E Collections.max(List<E> list,Comparator c)// E Collections.min(List<E> list,Comparator c)names.add("www");names.add("www");names.add("www");names.add("www");names.add("www");System.out.println("集合: " + names);System.out.println("集合长度: "+names.size());//调用工具类方法找出www出现的次数int count = Collections.frequency(names,"www");System.out.println("www出现的次数"+count);//9.将集合里的所有www替换成呜呜呜Collections.replaceAll(names,"www","呜呜呜");System.out.println(names);}
}
结果演示:
7.2 数组与集合的转换
1)集合转数组
集合转数组
Object[] ToArray();
作用:将集合转成Object[]。一旦想要使用数组中元素自己的方法和属性,还需要强转。
T[] toArray(T[] a)
作用:将集合转成数组。
只需要传入一个元素类型的数组对象,就可以返回元素类型的数组。
代码演示:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;public class CollectionsDemo02 {public static void main(String[] args) {List<String> names = new ArrayList<>();names.add("michael");names.add("ming");names.add("jack");Object[] arr1 = names.toArray();//获取第二个元素的第二个字符。Object obj = arr1[1];if(obj instanceof String){((String) obj).charAt(1);}// 使用第二种方法String[] arr = new String[0];String[] arr2 = names.toArray(arr);System.out.println(Arrays.toString(arr2));//System.out.println("第一个元素是不是el结尾:"+(arr2[0].endsWith("el")));System.out.println((arr2[0].indexOf("el")));}
}
2)数组转集合
数组转集合的案例演示:
1. 数组转成的集合对象,不能修改长度。
2. 如果想要进行增删操作,可以将转成的集合里的元素放入一个新的集合里,对新集合进行操作。
代码演示:
public class CollectionsDemo03 {public static void main(String[] args) {String[] names = new String[5];names[0]="michael";names[1]="jane";names[2]="mark";names[3]="marry";names[4]="rose";List<String> list = Arrays.asList(names);System.out.println(list);//将集合的第二个元素替换成张三Collections.replaceAll(list,list.get(1),"张三");
// list.set(1,"张三");//数组转成的集合不能改变长度。增加删除都会修改集合的长度。
// list.remove(2);
// list.add("sdads");List<String> newList = new ArrayList<>(list);
// newList.addAll(list);System.out.println(newList);newList.add("张三");newList.add("李四");System.out.println(newList);
// newList.
// String str = newList.get(0);Collections.swap(newList,0,newList.lastIndexOf("张三"));
// newList.set(0,"张三");
// newList.set(newList.lastIndexOf("张三"),str);System.out.println(newList);}
}
注意,数组转成的集合不能执行修改长度的操作,否则就会发生异常。要进行操作的话,可以在创建一个集合。
8.Map类
8.1 map简介
Map是集合框架中的另一个父接口,它用来保存具有映射(一对一)关系的数据,这样的数据称之为键值对(Key-Value-Pair)。key可以看成是value的索引。特点如下:
key和value必须是引用类型的数据
作为key,在Map集合中不允许重复,value可以重复
key可以为null,但只能一次。
key和value之间存在单向一对一关系,通过指定的key总能找到唯一,确定的value
根据内部数据结构的不同,Map接口有多种实现类,其中常用的有内部为hash表实现HashMap和内部为排序二叉树实现的TreeMap。
8.2 常用方法
1.Map接口,是集合框架中的另外一个父接口
2. Map存储的数据结构的特点: 一对一的关系映射,称之为Key-Value-Pair
3.Map 接口最常用的两个子类。是HashMap和TreeMap。
-HashMap: 底层使用了Hash表和红黑树的数据结构(JDK1.8之前使用的是hash表+单向链表)
-TreeMap: 底层使用的是二叉树
4.Map的key不能重复,但是可以为null,value可以重复
5.Map的key可以理解我时value的索引,总能通过一个key找到一个具体的value。
6.Map里的元素也是无序的(存储顺序和存入顺序无关)。
创建方法:(以HashMap为例)
Map<T, T> map = new HashMap<>();
添加方法: put(T key,T value);
获取元素方法: get(T key)
测试代码:
public class MapDemo01 {public static void main(String[] args) {
// 创建一个map集合(散列表)Map<String, Integer> map = new HashMap<>();
// 存储元素,调用put方法map.put("张三",100);map.put("李四",98);map.put("王五",97);map.put("赵六",99);
// 输出打印一下System.out.println(map);//2.获取一个人的成绩。调get(K k)Integer score= map.get("张三");System.out.println("张三的成绩:"+score);//存入张三和60分 小贴士: put方法,如果key相同,会覆盖掉原来的value。map.put("张三",60);System.out.println(map);//取出小八的成绩 get(Key) 如果key不存在,返回null。Integer s1 = map.get("小八");System.out.println("小八的成绩: "+s1);//测试key是否可以为null,可以,只能有一个。map.put(null,0);System.out.println(map);}
}
- boolean isEmpty():判断是否为空
- containsKey(Object o): 是否存在o这个key值
- containsValue(Object o):是否存在o这个value值
- V remove(K k):移除一个键值对。
- int size():图的大小
- clear(): 清空
代码演示如下:
public class MapDemo02 {public static void main(String[] args) {HashMap<String,String> course = new HashMap<>();course.put("王老师","语文");course.put("李老师","数学");course.put("张老师","化学");course.put("赵老师","生物");System.out.println(course);/*** 1.boolean isEmpty()*/System.out.println("course 集合是否为空:"+course.isEmpty());/*** 2.containsKey(Object o)*/System.out.println("是否包含张老师这个key:"+course.containsKey("张老师"));/*** 3.containsValue(Object o)*/System.out.println("是否包含数学这个value:"+course.containsValue("数学"));/*** 4. V remove(K k)移除一个键值对。*/course.remove("赵老师");System.out.println(course);/*** 5.int size()*/System.out.println(course.size());/*** 6.void clear():清空所有的键值对*/course.clear();System.out.println(course.size());System.out.println(course);}
}
8.3 Map的遍历
方法:
第一种方式: 使用 keySet(): 返回所有的key的set集合形式。
第二种方式: 使用 entrySet(): 返回entry对象的Set集合。
Entry: 是Map的内部类,Entry对象就是封装了一个键值对。第三种方式: 使用values(): 返回所有的value的Collection。
代码如下:
public class MapDemo03 {public static void main(String[] args) {Map<String,Integer> scores = new HashMap<String,Integer>();scores.put("michael",100);scores.put("lucy",98);scores.put("tom",97);scores.put("John",99);/*** 第一种方式: 使用 keySet(): 返回所有的key的set集合形式*/Set<String> keys = scores.keySet();for(String key:keys){//通过key获取valueInteger value = scores.get(key);System.out.println(key+"="+value);}System.out.println("-------------------------------");/*** 第二种方式: 使用 entrySet(): 返回entry对象的Set集合* Entry: 是Map的内部类,Entry对象就是封装了一个键值对。*/Set<Map.Entry<String,Integer>> es = scores.entrySet();for(Map.Entry<String,Integer> entry:es){//每一个entry都是一个键值对//System.out.println(entry);/*** Entry类型提供了getKey()和getValue()方法*/String key = entry.getKey();Integer value = entry.getValue();System.out.println(key+"="+value);}System.out.println("-----------------------");/*** 第三种方式: 使用values(): 返回所有的value的Collection*/Collection<Integer> values = scores.values();for (Integer value : values) {System.out.println(value);}}
}
8.4 HashMap的实现原理
1)原理
HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。
图中,紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。
2)装载因子及其HashMap优化
- capacity:容量,hash表里bucket(桶)的数量,也就是散列数组大小
- initial capacity:初始容量,创建hash表时,初始bucket的数量,默认构建容量为16,也可以使用特定容量。
- size:大小,当前散列表中存储数据的数量
- load factor:加载银子,默认值0.75也就是75%,当向散列表增加数据时,如果size/capacity的值大于loadfactor,则发生扩容并且重新散列(rebash)
- 性能优化:加载因子较小时,散列查找性能会提高,但是也浪费了散列桶空间容量。0.75是性能和空间相对平衡结果。在创建散列表时指定合理容量,减少rehash能提供性能
8.5 HashTable 与HashMap
HashMap 和 Hashtable 是 Map 接口的两个典型实现类
区别:
Hashtable 是一个古老的 Map 实现类,不建议使用
Hashtable 是一个线程安全的 Map 实现,但 HashMap 是线程不安全的。
Hashtable 不允许使用 null 作为 key 和 value,而 HashMap 可以
与 HashSet 集合不能保证元素的顺序的顺序一样,Hashtable 、HashMap 也不能保证其中 key-value 对的顺序
8.6 LinkedHashMap
LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致
LinkedHashMap:
1.是HashMap的子类型
2.使用链表维护了元素的插入顺序
测试代码:
public class MapDemo04_LinkedHashMap {public static void main(String[] args) {Map<String,String> map = new LinkedHashMap<> ();map.put("张三", "北京");map.put("李四", "上海");map.put("王五", "北京");map.put("赵六", "长春");System.out.println(map);}
}
8.7 TreeMap
TreeMap 存储 Key-Value对时,需要根据 Key 对 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
TreeMap 的 Key 的排序:
自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口
代码演示:
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;/*** TreeMap:* 1.使用二叉树对key进行排序,来维护整个集合的KV对的顺序* 2.默认是升序,可以通过比较器进行自定义排序。*/
public class MapDemo05_TreeMap {public static void main(String[] args) {Comparator c1 = new Comparator<String>() {public int compare(String o1, String o2) {return -o1.compareTo(o2);}};Map<String,Integer> map = new TreeMap<String,Integer>(c1);map.put("A", 100);map.put("B", 22);map.put("C", 32);map.put("D", 44);System.out.println(map);}
}
8.8 properties
Properties 类是 Hashtable 的子类,该对象用于处理属性文件
由于属性文件里的 key、value 都是字符串类型,所以 properties 里的 Key 和 Value 都是字符串类型的。
Properties
- 是HashTable的子类型,比较常用,一般用于加载配置文件里的KEY和VALUE。
- 因为配置文件里都是字符串,因此Properties里的KEY和VALUE耶都是String类型。
- 该对象的value和key都不能为null。
测试代码:
import java.util.Map;
import java.util.Properties;/*** Properties* 1.是HashTable的子类型,比较常用,一般用于加载配置文件里的KEY和VALUE2.因为配置文件里都是字符串,因此Properties里的KEY和VALUE耶都是String类型。3.该对象的value和key都不能为null。*/public class MapDemo06_Properties {public static void main(String[] args) {//创建一个配置文件属性对象Properties prop = new Properties();//添加几个key和valueprop.setProperty("url","jdbc:mysql://localhost:3306/mydb");prop.setProperty("user","root");prop.setProperty("password","123456");prop.setProperty("driver","com.mysql.jdbc.Driver");System.out.println(prop);System.out.println(prop.size());/*** properties的遍历*/for (Map.Entry<Object, Object> entry : prop.entrySet()) {System.out.println(entry.getKey() + "=" + entry.getValue());}/*** 通过指定的key,获取value,如果key不存在,返回null。*/String url = prop.getProperty("url");System.out.println(url);/*** getProperty(String key,String defaultValue)* 逻辑: 通过指定的key,获取对应的value值,如果该key不存在,就返回默认值。*/String p1 = prop.getProperty("school","yc小学");System.out.println(p1);}
}
集合真的好多哇,记不住。┭┮﹏┭┮