集合框架
Java是面向对象编程,万事万物皆“对象”,为了方便对“对象”进行操作,需要对“对象”进行存储,而Java集合就是存储“对象”的容器,可以动态地把多个对象存入容器中。Java集合类可以用于存储数量不等的多个对象,还可以用于保存具有映射关系的关联数组。
Collection接口
-
单列集合,用来存储”一个一个“的对象。
-
向Collection接口的实现类的对象添加数据(obj)时,要求obj所在的类重写equals()。(不重写的话Collection中有些方法的判断会有影响。)
-
Collection接口中的方法:
- add(Object obj):添加一个对象。
- addAll(Collection coll):添加一个集合。
- int size():获取集合内有效元素的个数。
- void clear():清空集合。
- boolean isEmpty():判断集合是否为空。
- boolean contains(Object obj):判断集合中是否包含obj对象。(调用equals()进行判断)
- boolean containsAll(Collection coll):判断集合中是否包含coll集合内的所有对象。(调用equals()进行判断)
- boolean remove(Object obj):删除集合中第一个与obj对象相同的对象元素。(调用equals()进行判断)
- boolean removeAll(Collection coll):取当前集合与coll集合的差集。
- boolean retainAll(Collection coll):取当前集合与coll的交集。
- boolean equals(Object obj):判断集合是否相等。(注意有序性和无序性)
- hashCode():获取集合对象的哈希值。
- Object[] toArray():将集合转换成Object类型数组。(调用Arrays.asList(),可以把数组转换成集合)
- iterator():返回迭代器对象,用于集合的遍历。
迭代器使用注意点:(迭代器是设计模式中的一种)
Collection接口实现了java.lang.Iterable接口,该接口中有iterator(),所有实现了Collection接口的实现类都有一个iterator()方法。
- 集合对象每次调用iterator(),都会得到一个全新的迭代器对象,默认游标在第一个元素之前。
- Iterator仅用于遍历集合,Iterator本身并不提供承装对象的能力。
- 迭代器中的重要的两个方法:hasNext()和next()。
- hasNext()判断是否还有下一个元素。
- 调用next()后,迭代器游标下移,并返回下移之后集合位置上对应的元素。如果没有使用hasNext()判断下一条记录是否有效,且下一条记录无效,调用next()后,则抛出异常(NoSuchElementException)。
- 迭代器也可以删除集合中的元素,调用的是迭代器中的remove(),不是集合的remove()。
- 如果没有调用next()或在上一次调用next()方法后已经调用了remove(),再次调用remove()都会抛出异常(IllegalStateException)。
迭代器遍历集合示例:
@Test
public void test3(){
Collection c = new ArrayList();
c.add(123);
c.add(3.14);
c.add('A');
Iterator iterator1 = c.iterator();
while (iterator1.hasNext()) {
System.out.println(iterator1.next());
}
System.out.println("=========================================");
Iterator iterator2 = c.iterator();
while (iterator2.hasNext()){
Object o = iterator2.next();
if (o.equals(123)){
iterator2.remove();
}
}
Iterator iterator3 = c.iterator();
while(iterator3.hasNext()){
System.out.println(iterator3.next());
}
}
List接口
- 存放的对象特点:有序,可重复。
- List接口下有三个实现类:ArrayList、LinkedList、Vector。
List接口中新提供的方法:
- void add(int index,Object obj):在下标为index位置插入obj元素。
- boolean addAll(int index,Collection coll):从下标为index处,将集合coll所有的元素添加进来。
- Object get(int index):获取下标为index的元素。
- int indexOf(Object obj):返回obj对象在当前集合第一次出现的位置下标。
- int lastIndexOf(Object obj):返回obj对象在当前集合最后一次出现的位置下标。
- Object remove(int index):删除index位置对应的元素,并返回该元素。(注意区分Collection接口下的remove())
- Object set(int index,Object obj):设置指定index位置的元素为obj。
- List subList(int fromIndex,int toIndex):返回从fromIndex到toIndex位置的子集合。
ArrayList
-
适用于经常需要进行查询操作的对象集合。
-
JDK1.7及1.2之前,new ArrayList();创建ArrayList对象,底层就创建了长度为10的Object[]数组elementData。JDK1.8后,new ArrayList();创建ArrayList对象,底层Object[] elementData初始化为{},并没有创建长度为10的数组,直到ArrayList对象第一次调用add()时,才创建长度为10的数组,并把add()的参数添加到数组中。
-
底层为用Object[] elementData数组进行存储。不带参数的构造方法创建的ArrayList对象默认容量为10(initialCapacity),也可以使用带参构造方法创建ArrayList的对象,创建的时候就指定容量。
-
扩容:变为原来的1.5倍(原容量加上原容量右移1位,即1+(1/2)),并把原数组的元素复制到新数组中。
- 查询效率高;插入、删除效率低。
Arrays.asList()使用注意点
//asList()注意点
@Test
public void test2(){
Collection coll = new LinkedList();
coll.add(123);
List list1 = Arrays.asList(new int[]{1, 2, 3, 4, 5, 6, 7});
// 不自动装箱的话把整个int[]当成一个对象
System.out.println(list1.size()); //1
// 需要手动装箱
List list2 = Arrays.asList(new Integer[]{1,2,3,4,5,6,7});
System.out.println(list2.size()); //7
System.out.println(list2);
}
LinkedList
-
使用于经常需要插入和删除操作的对象集合。
-
底层用双向链表进行存储,没有定义数组,定义了Node类型的first和last记录首末元素,同时定义内部类Node,作为LinkedList中保存数据的基本结构,还定义了两个变量prev和next,分别记录前一个和后一个元素的位置。
Vector
线程安全,效率低,底层使用Objece[] elementData数组进行存储。
在JDK1.7和1.8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来数组的两倍。
Set接口
- 存放对象的特点:无序(存入和取出的顺序可能不一致),不可重复,如果尝试添加两个相同的元素,则添加操作失败。
- Set接口下的实现类有:HashSet,LinkedHashSet,TreeSet。
- Set接口没有提供额外的方法,所以用的都是Collection接口中的方法。
- Set中的元素判断两个对象是否相同用的是equals(),而不是“==”。
- 对于存放在Set容器中的对象:一定要重写hashCode()和equals(),以实现对象相等规则,即相等的对象必须具有相等的散列码(哈希值)。
重写hashCode()的原则:
- 程序运行时,同一个对象多次调用hashCode()应该返回相同的值。
- 当两个对象的equals()比较返回true时,两个对象的hashCode()返回值也应该相同。
- 对象中用作equals()方法比较的属性,都应该用来计算hashCode值。
- hashCode()的默认行为是对堆上的对象产生独特值,如果没有重写hashCode(),则该class的两个对象的hashcode无论如何都不会相等,即使这两个对象的数据相同。
重写equals()的原则:
- 当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类改写后的equals(),两个截然不同的对象有可能在逻辑上是相等的,但是根据Object.hashCode(),他们仅仅是两个对象,因此违反了“相等的对象必须具有相等的散列码”。
- 重写equals()方法的时候一般需要重写hashCode(),通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。
HashSet
- HashSet底层实现是:HashMap。底层存储结构是数组,初始容量为16,扩容为原来的2倍。
- 不能保证元素的排列顺序。(不意味着每次遍历集合,对象的顺序都会改变)
- HashSet不是线程安全的。
- 可以存储null值。
示例:
@Test
public void test1(){
HashSet set = new HashSet();
set.add(123);
set.add(456);
set.add("abc");
set.add(null);
set.add(5858);
set.add(5858);
System.out.println(set);
}
LinkedHashSet
-
LinkedHashSet底层实现是:LinkedHashMap。
-
LinkedHashSet不是线程安全的。
-
LinkedHashSet是HashSet的子类。
-
以元素插入的顺序来维护集合的链表。
-
LinkedHashSet类里面添加了两个引用,用来记录当前元素的前一个元素和后一个元素(双向链表),因此LinkedHashSet可以按照元素添加的前后顺序遍历集合中所有的元素(这并不意味着有序性)。
-
对于频繁遍历操作,LinkedHashSet的效率高于HashSet。插入元素的性能略低于HashSet。
-
可以存储null值。
示例:
@Test
public void test1(){
LinkedHashSet set = new LinkedHashSet();
set.add(123);
set.add(456);
set.add("abc");
set.add(null);
System.out.println(set);
}
TreeSet
- TreeSet底层实现是:红黑树。
- 只能存储相同类型的引用类型数据。
- 有两种排序方式,自然排序和定制排序(涉及Comparable和Comparator),默认情况下为自然排序。
- TreeSet不是线程安全的。
- 不可以存储null值,会抛出异常(NullPointException)。
示例:TreeSet按照两种排序存储数据。
Employee类:实现了Comparable接口。
/**
* @Author:xiezr
* @Creat:2021-07-14 20:57
*/
public class Employee implements Comparable {
private String name;
private int age;
private MyDate birthday;
public Employee() {
}
public Employee(String name, int age, MyDate birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}
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 MyDate getBirthday() {
return birthday;
}
public void setBirthday(MyDate birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
", birthday=" + birthday +
'}' +
'\n';
}
// 按名称从低到高排序,再按年龄从高到低排序
@Override
public int compareTo(Object o) {
if (o instanceof Employee){
Employee e1 = (Employee) o;
if (this.name.equals(e1.name)){
return -this.age - e1.age;
}else {
return this.name.compareTo(e1.name);
}
}
throw new RuntimeException("输入的类型错误!");
}
}
MyDate类:无实现Comparable或Comparator接口。
/**
* @Author:xiezr
* @Creat:2021-07-14 20:57
*/
public class MyDate {
private int year;
private int month;
private int day;
public MyDate() {
}
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
测试:
import org.junit.Test;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
/**
* @Author:xiezr
* @Creat:2021-07-14 21:17
*/
public class TreeSetTest {
// 按名称从低到高排序,再按年龄从高到低排序
@Test
public void test1(){
Employee e1 = new Employee("Adel", 25, new MyDate(1998, 8, 31));
Employee e2 = new Employee("Maroon5",35,new MyDate(1987,6,6));
Employee e3 = new Employee("LinKinPark",28,new MyDate(1999,2,9));
Employee e4 = new Employee("Passenger",27,new MyDate(1999,5,6));
Employee e5 = new Employee("Eminem",40,new MyDate(1979,2,6));
Employee e6 = new Employee("Adel", 52, new MyDate(1945, 8, 31));
TreeSet treeSet = new TreeSet();
treeSet.add(e1);
treeSet.add(e2);
treeSet.add(e3);
treeSet.add(e4);
treeSet.add(e5);
treeSet.add(e6);
System.out.println(treeSet);
}
// 按照出生年份从大到小排,月份天数从小到大排
@Test
public void test2(){
Employee e1 = new Employee("Adel", 25, new MyDate(1998, 8, 31));
Employee e2 = new Employee("Maroon5",35,new MyDate(1987,6,6));
Employee e3 = new Employee("LinKinPark",28,new MyDate(1999,2,9));
Employee e4 = new Employee("Passenger",27,new MyDate(1999,2,6));
Employee e5 = new Employee("Eminem",40,new MyDate(1979,2,6));
Employee e6 = new Employee("Adel", 52, new MyDate(1945, 8, 31));
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof Employee && o2 instanceof Employee){
Employee e1 = (Employee) o1;
Employee e2 = (Employee) o2;
MyDate m1 = e1.getBirthday();
MyDate m2 = e2.getBirthday();
int minusYear = m1.getYear() - m2.getYear();
if (minusYear != 0){
return -minusYear;
}else {
int minusMonth = m1.getMonth() - m2.getMonth();
if (minusMonth != 0){
return minusMonth;
}else {
return m1.getDay() - m2.getDay();
}
}
}
throw new RuntimeException("输入类型异常!");
}
});
treeSet.add(e1);
treeSet.add(e2);
treeSet.add(e3);
treeSet.add(e4);
treeSet.add(e5);
treeSet.add(e6);
Iterator iterator = treeSet.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
Map接口
- 双列,用来存储的一对一对(key-value)的对象。
- Map中的key是无序的,不重复的,使用Set存储所有的key,所以key所在的类要重写hashCode()和equals()。
- Map中的value是无序的,可重复的,使用Collection存储所有的value,所以value所在的类要重写equals()。
- 一个键值对key-value构成一个Entry对象,Map中的entry是无序的,不可重复的,使用Set存储所有的entry对象。
Map接口中常用的方法
- 添加、删除、修改操作:
- Object put(Object key,Object value):将指定的key和value添加到当前map对象中,如果key相同,则value覆盖掉原来的value。
- putAll(Map map):把map中所有的key-value键值对存放到当前集合对象中。
- Object remove(Object key):删除指定key对应的key和value,并返回被删除的value。
- void clear():清除当前集合对象中所有的数据。
- 查询操作:
- Object get(Object key):根据指定的key,获取对应的value。
- containsKey(Object key):判断当前集合对象是否含有指定的key。
- containsValue(Object value):判断当前集合对象是否含有指定的value。
- int size():返回集合对象中的key-value个数。
- boolean isEmpty():判断当前集合对象是否为空。
- boolean equals(Object obj):判断当前集合对象和obj对象是否相等。
- 元视图操作(用于遍历):
- Set keySet():返回所有key构成的Set集合。
- Collection Values():返回所有value构成的Collection集合。
- Set entrySet():返回所有key-value构成的Set集合。
HashMap
- HashMap作为Map接口的主要实现类,底层的实现是:数组+链表+红黑树(JDK1.8)。(JDK1.7及之前,底层的实现是数组+链表)
- new HashMap():底层并没有创建一个长度为16的数组,当HashMap的对象首次调用put()添加数据时,创建长度为16的数组。(不同于JDK.7)
- 底层的数组默认容量为16,需要扩容则扩容为原来的两倍(*2),并将原有的数据赋值过来,此时数据的存放位置可能跟在原先数组的存放位置不一致(需要重新计算元素在新数组中的位置,再进行存储)。
- 底层数组是Node[],而非Entry[]。(JDK1.7和1.8的区别之一)
- 以key-value这种键值对的形式存储数据。
- 线程不安全,效率高。可存储null的key和value。
HashMap底层的实现:map.put(key1,value1);
首先,调用key1所在类的hashCode()计算key1的哈希值,然后该哈希值经过某种算法计算以后,得到在Node[]数组中的存放位置(即索引位置)。
情况1:如果该位置的数据为空,此时key1-value1添加成功。(以数组的方式存储)
情况2:如果此位置上的数据不为空,意味着此位置上存在一个或多个数据(以链表的方式存在),此时,比较key1和已经存在的一个或多个数据的哈希值,如果key1的哈希值与已存在的任何一个数据都不相同,则key1-value1添加成功。(以链表的方式存储)
情况3:如果key1的哈希值与已存在的某一个数据(key2-value2)的哈希值相同,继续进行比较,调用key1所在类的equals(key2),如果equals()返回false,则key1-value1添加成功。(以链表的方式存储)
情况4:如果equals()返回true,则使用value1覆盖掉value2。
注意:当Node[]数组某一索引位置上的元素以链表存在的形式的数据个数大于8,且此时数组的长度大于64时,此时索引位置上的所有数据改为用红黑树存储。
HashMap的扩容:
扩容涉及到容量,装填因子以及临界值。底层Node[]数组的默认容量为16,默认装填因子为0.75,临界值为容量*装填因子,即默认为12。
当往HashMap集合中添加数据,此时数据以数组的方式存储,而不是以链表的方式存储时,添加数据后,Node[]的长度大于临界值(12)时,Node[]数组进行扩容,*2,容量变成原来的两倍即32,然后把原有的数据赋值,添加到扩容后的数组中,此时临界值重新计算,变成了24。
装填因子的大小对HashMap的影响:
装填因子的大小决定HashMap数据的密度。
装填因子越大,密度越大,发生冲突(不同元素,哈希值相同)的几率越大,数组中的链表越容易长,造成查询或插入时比较的次数增多,性能会下降。
装填因子越小,临界值越小,越容易引发扩容,数据密度也越小,意味着发生冲突的几率越小,数组中的链表也越短,查询和插入时比较的次数也越少,性能会更高,但是会浪费一定的内存空间,而且经常扩容也会影响性能。
按照其他语言的参考以及研究经验,考虑将装填因子设置为0.7~0.75,此时平均检索长度接近于常数。
HashMap的遍历操作:
public class HashMapTest {
@Test
public void test1(){
HashMap hashMap1 = new HashMap();
// 往当前集合对象中添加数据
hashMap1.put(1001,"旺财");
hashMap1.put(1002,"来福");
hashMap1.put(1003,"富贵");
HashMap hashMap2 = new HashMap();
hashMap2.put(1004,"常威");
// 将形参集合中的所有key-value添加到当前集合对象中
hashMap2.putAll(hashMap1);
System.out.println(hashMap2);
// 移除指定的key对应的key-value
// hashMap2.remove(1003);
// 遍历集合中的key-value
// 方式一:两个迭代器分别获取key和value
Set set = hashMap2.keySet();
Collection values = hashMap2.values();
Iterator iterator1 = set.iterator();
Iterator iterator2 = values.iterator();
// while (iterator2.hasNext()){
// System.out.println(iterator2.next());
// }
while (iterator1.hasNext() && iterator2.hasNext()){
System.out.println(iterator1.next() + "====" + iterator2.next());
}
System.out.println();
// 方式二:通过Entry的getKey()和getValue()获取key和value的值
Set set1 = hashMap2.entrySet();
Iterator iterator = set1.iterator();
while (iterator.hasNext()){
Map.Entry entry = (Map.Entry) iterator.next();
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
System.out.println();
// 方式三:通过迭代器获取key的值,调用Map接口下的get()方法,根据Key获取value的值。
Iterator iterator3 = set.iterator();
while (iterator3.hasNext()){
Object key = iterator3.next();
Object value = hashMap2.get(key);
System.out.println(key + "<====>" + value);
}
}
}
LinkedHaspMap
- LinkedHashMap是HashMap的子类,底层实现于HashMap一样。
- LinkedHashMap类里面定义了内部类Entry,该内部类继承了HashMap.Node<k,v>,Entry类里边定义了两个变量before和after,用于记录当前元素的前一个元素和后一个元素,使得LinkedHashMap可以按照元素添加的顺序进行遍历。
TreeMap
- TreeMap是按照元素的key值进行排序的,因此要求用TreeMap集合存储的对象的key值必须是相同类型。
- TreeMap可以根据key实现排序遍历,此时考虑key的自然排序或者定制排序。(涉及Comparable和Comparator接口)
Hashtable
-
古老实现类,JDK1.0就存在,在JDK1.2之后被HashMap代替。
-
线程安全,效率低。不可以存储null的key和value。
Properties
- 常用来处理配置文件,key和value都是String类型的。
- 存取数据时,建议使用setProperty(String key,String value)和getProperty(String key)。
Collections工具类
- Collections是用于操作Collection(List、Set)和Map的工具类。
Collections中的常用方法:
- 排序操作:(均为static方法)
- reverse(List list):反转list集合中的所有元素。
- shuffle(List list):对list集合中的所有元素进行随机排序。
- sort(List list):根据元素的自然顺序对指定list集合按升序排序。
- sort(List list,Comparator com):根据com定制的排序对指定的list集合进行定制排序。
- swap(List list,int i,int j):将list集合中下标为i的元素和下标为j的元素进行交换。
- 查找、替换:
- Object max(Collection coll):根据元素的自然排序,返回给定集合中的最大元素。
- Object max(Collection coll,Comparator com):根据com的定制排序,返回给定集合中的最大元素。
- Object min(Collection coll):根据元素的自然排序,返回给定集合中的最小元素。
- Object min(Collection coll,Comparator com):根据com的定制排序,返回给定集合中的最小元素。
- int frequency(Collection coll,Object obj):返回指定对象obj在指定集合coll中出现的次数。
- boolean replaceAll(List list,Object oldVal,Object newVal):使用newVal替换集合list中的oldVal。
- void copy(List dest,List src):将src中的内容复制到dest中。(注意底层的实现)
- copy()注意点:
public class CopyTest {
@Test
public void test1(){
ArrayList list = new ArrayList();
list.add(626);
list.add(628);
list.add("I'm");
list.add("Iron");
list.add("Man");
// ArrayList list1 = new ArrayList();
// 错误,IndexOutOfBoundsException: Source does not fit in dest,
List list1 = Arrays.asList(new Object[list.size()]);
Collections.copy(list1,list);
System.out.println(list1);
}
}
这里的dest相当于list1,src相当于list,如果按照注释掉的代码,一开始,list1.size()是为0的,即dest.size()为0,list中有五个元素,即src.size()为5,即srcSize为5,那么就会抛出异常,所以,要进行复制操作时,可以给空的新集合里面填一个Object[]数组,撑起里面的size,给Object[]数组的长度赋值为被复制的集合的size(),即被复制的集合中有x个元素,就用一个长度为x的Object[]把新数组的size撑起来等于被复制的集合的size。
- 将指定集合包装成线程同步的集合:(可以解决多线程并发访问集合中的安全问题)
- synchronizedList():将指定集合包装成线程同步的List集合。
- synchronizedSet():将指定集合包装成线程同步的Set集合。
- SynchronizedMap():将指定集合包装成线程同步的Map集合。
即使ArrayList和HashMap是线程不安全的,Vector和Hashtable是线程安全的,但是实际开发中依旧不会去似乎用Vector和Hashtable,而是调用Collections工具类下的指定方法,把ArrayList和HashMap等包装成线程安全的,然后再使用。