1 集合概述
集合: 集合是 Java 提供的一种容器,同数组一样用来存储多个数据。
数组和集合的区别:
- 数组的长度是固定的。集合的长度是可变的(动态扩容)。
- 数组中元素的类型一致,一般用来存储基本数据类型。集合一般用来存储对象,且对象的类型可以不一致。
2 集合框架
集合按照其存储结构可以分为两类:
- 单列集合
java.util.Collection
,存储单值的数据。 - 双列集合
java.util.Map
,以键值对的形式存储双值数据。
集合中最大的几个操作接口:Collection、Map、Iterator。
集合结构:
3 Collection接口
Collection 接口是所有单列集合的父接口。
接口定义: public interface Collection<E> extends Iterable<E>
接口常用方法:
开发中一般不会直接使用 Collection 接口,主要使用它的子接口:List,Set。
- List :元素有序,元素可重复。
- Set :元素无序,元素不可重复。
3.1 List接口
List 集合是单列集合中的一个重要分支,List集合中的元素是可以重复的,它存储的元素是有序的,即数据的存入顺序与取出顺序一致,它以线性的方式进行存储,类似一个数组的结构。
List 接口特点:
- 元素存取有序。
- 集合带有索引,可以通过索引精确操作集合中的元素。
- 集合中的元素允许重复。
接口定义: public interface List<E> extends Collection<E>
List 接口在继承自 Collection 接口的基础上进行了方法上的扩充,以下为扩充方法:
补充: 使用 add 方法添加数据时会将索引处的数据往后挤,即索引处开始所有数据后移一位。
想使用 List 接口需要借助该接口的实现类,有以下三个实现类:ArrayList、Vector、LinkedList。
3.1.1 ArrayList
ArrayList 以数组的形式存储数据,且是一个可以动态扩容的数组。添加和删除数据很慢,查找数据很快。此类的 iterator 方法返回的迭代器是快速失败的(具体参考下述 Iterator)。
类名:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
3.1.1.1 ArrayList的动态扩容
构造方法:
无参构造方法描述说初始容量为10,这个描述不够准确,下面为无参构造方法源码:
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
那么这个10从哪里来的呢?动态扩容又是如何实现的呢?这些都在添加数据时发生,即使用 add 方法时发生。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
在调用 add 方法后,将集合修改次数变量 modCount 自增1,然后调用add(e, elementData, size)
,其中 e 为添加的数据(使用了泛型,在声明时已确定),elementData 为要存储的目标集合,size 为集合中有效数据的个数,最后需要注意添加数据一定返回true。
下面进入到add(e, elementData, size)
中:
/**
* This helper method split out from add(E) to keep method
* bytecode size under 35 (the -XX:MaxInlineSize default value),
* which helps when add(E) is called in a C1-compiled loop.
*/
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
判断有效数据个数是否等于数组长度,即数组是否满了,满了则调用扩容方法 grow()将数组进行扩容,没满则在末尾存入数据并将有效数据个数加一。
下面进到扩容方法 grow():
private Object[] grow() {
return grow(size + 1);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
* @throws OutOfMemoryError if minCapacity is less than zero
*/
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
进入无参 grow 方法后调用有参 grow 方法,参数为最小需要的容量大小 capacity ,该数值为有效数据个数加一。
进入有参 grow 方法后调用 Arrays 工具类的 copy 方法将原数组的内容复制到新数组中并返回新数组。
新数组的长度通过调用newCapacity(minCapacity)
方法产生,其中 minCapacity 为最小容量大小。
下面进入newCapacity(minCapacity)
中:
/**
* Returns a capacity at least as large as the given minimum capacity.
* Returns the current capacity increased by 50% if that suffices.
* Will not return a capacity greater than MAX_ARRAY_SIZE unless
* the given minimum capacity is greater than MAX_ARRAY_SIZE.
*
* @param minCapacity the desired minimum capacity
* @throws OutOfMemoryError if minCapacity is less than zero
*/
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* The maximum size of array to allocate (unless necessary).
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
首先获取原数组空间大小 oldCapacity,计算新数组空间大小 newCapacity,它的大小为 oldCapacity 的 1.5 倍 ,其中 (oldCapacity >> 1) 为位运算,数据右移一位表示除以 2。
进入第一个 if 判断新空间大小 newCapacity 是否是否比最小需要空间大小 minCapacity 要小。 (扩容新空间是否够用?)
(扩容新空间不够用) 当 newCapacity 小于等于 minCapacity 时,判断原数组是不是使用无参构造器时产生的默认的零空间大小数组,如果是则在 DEFAULT_CAPACITY 与 最小需要空间大小 minCapacity 中选择一个最大值返回,初次使用时数组大小为 0,则 minCapacity为 1,那么最大值为 10 ,如此无参构造器描述的初始容量为 10 的空列表产生了。如果原数组不是默认的零空间数组则判断 minCapacity 是否小于零( int 数据在计算机中以二进制存储,int 的最大值加一将变成 int 的最小值,该值为负数),即数据溢出,溢出则抛出异常,无溢出则最后返回最小需要空间大小 minCapacity 作为新的扩容数组的空间大小。
什么时候会产生扩容新空间不够用问题呢?
- 使用无参构造器,第一次调用 add,初始零空间扩容 1.5 倍还是零空间。
- 指定空间大小为 1 时,1 扩容还是 1。
- 添加的是一组数据,最小需要空间大小不可控,可能出现原空间扩容 1.5 倍仍不够用的情况。
(扩容新空间够用,且不超范围) 当 newCapacity 大于 minCapacity 时,判断扩容新空间大小 newCapacity 是否小于等于默认的最大数组长度 MAX_ARRAY_SIZE,该值为 int 最大值减 8 。是则返回 扩容新空间大小 newCapacity ,即将原空间扩大 1.5 倍。 (扩容空间够用,且超范围) 不是则调用hugeCapacity(minCapacity)
方法获得新的大小并返回。
进入hugeCapacity(minCapacity)
,判断 minCapacity 是否溢出,是则抛出异常,不是则判断最小需要空间 minCapacity 是否超范围,超范围则返回 int 的最大值作为新空间大小,没超范围则返回默认最大数组长度作为新空间大小。
提高ArrayList的使用效率: 动态扩容很方便,我们不需要关心数组的大小,它会自动随我们的需求扩建数组,然而每一次重建都是有所消耗的,频繁的重建会使得程序执行效率低下。所以当能够确定数组长度的大概范围时使用给定的 initialCapacity 来实例化对象可以省去一些重建数组的操作从而提高效率。
3.1.1.2 ArrayList的使用
使用案例:
package work.java.xzk10301003;
import java.util.ArrayList;
/**
*@ClassName: ArrayListTest
*@Description: ArrayList的使用
*
*/
public class ArrayListTest {
static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public static void main(String[] args) {
ArrayList arrayList = new ArrayList(); //使用无参构造器实例化ArrayList对象
// 此时arrayList集合中可以添加不一样的数据类型,基本数据类型,自定义类型均可
arrayList.add(1); //第一次使用add会将数组大小动态扩容到10
arrayList.add(1.1);
arrayList.add("hello");
arrayList.add(true);
arrayList.add(new Person("Mike", 18)); //添加自定义的匿名类对象
// 遍历集合索引,使用get方法获取指定索引数据并打印
for (int i = 0; i < arrayList.size(); i++) {
System.out.println(arrayList.get(i));
}
ArrayList<Integer> arrayList2 = new ArrayList<>(); //使用泛型实例化对象
// 此时arrayList2中只能添加Integer类型数据,使用时会自动装箱拆箱
arrayList2.add(1); //使用的add方法继承自Collection接口
arrayList2.add(1); //可以添加重复数据
arrayList2.add(2);
arrayList2.add(3);
arrayList2.add(2);
System.out.println(arrayList2); //调用了对象的toString方法
arrayList2.add(0, 0); //使用的add方法为List接口单独定义
System.out.println("索引0处添加数据0:");
System.out.println(arrayList2);
//使用的remove方法为List接口单独定义,删除指定索引的数据,返回值为该数据,类型为泛型指定
Integer i = arrayList2.remove(1);
System.out.println("删除索引1处的数据:");
System.out.println(arrayList2);
//使用的remove方法继承自Collection接口,删除集合中第一个匹配的指定的对象,返回值为boolean类型
boolean confirm = arrayList2.remove(Integer.valueOf(2));
System.out.println("删除第一个值为2的数据:");
System.out.println(arrayList2);
//使用set方法为List接口单独定义,将指定索引处的内容修改为指定内容,返回修改前的内容
Integer j = arrayList2.set(3, 4);
System.out.println("将索引3处的数据修改为4:");
System.out.println(arrayList2);
System.out.println("索引3处原数据:" + j);
}
}
3.1.2 Vector
与 ArrayList 一样,Vector 是 List 接口的子类。使用数组结构,添加和删除数据很慢,查找数据很快。此类的 iterator 方法返回的迭代器是快速失败的(具体参考下述 Iterator)。
类名:
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
3.1.2.1 Vector的动态扩容
构造方法:
Vector 的扩容方式同 ArrayList 基本一致,只有以下区别:
- Vector 有一个两参构造器,其中 CapacityIncrement 参数表示扩容增量,在执行扩容时,ArrayList 将旧长度扩容 1.5 倍,Vector 将旧长度增加 CapacityIncrement (只在 CapacityIncrement > 0 时生效)。
- Vector 没有指定扩容增量 CapacityIncrement,即默认 CapacityIncrement = 0,或者指定了增量但是 CapacityIncrement < 0 ,这两种情形下,Vector 将旧长度扩容 2 倍 。
- ArrayList 在第一次使用 add 时将初始化数组大小为 10 ,Vector在使用无参构造器时直接初始化数组大小为 10 。
3.1.2.2 Vector的使用
Vector 在使用上与 ArrayList 没有任何别。使用案例参考上述 ArrayList 即可。
3.1.3 Vector类与ArrayList类的区别
3.1.4 LinkedList
LinkedList 使用双向链表的结构存储数据,添加和删除数据很快,查找数据很慢。基础的操作方式也都继承自 List 接口和 Collection 接口,同时添加了一些自己独有的方法。
类名:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable
常用新添方法:
变量和类型 | 方法 | 描述 |
---|---|---|
void | addFirst(E e) | 在集合的开头添加一个指定元素。 |
void | addLast(E e) | 在集合的末尾添加一个指定元素。 |
E | getFirst() | 返回集合中的第一个元素,集合为空则抛出异常。 |
E | getLast() | 返回集合中的最后一个元素,集合为空则抛出异常。 |
boolean | offer(E e) | 在集合的末尾添加一个指定元素,返回一定为true。 |
boolean | offerFirst(E e) | 在集合的开头添加一个指定元素,返回一定为true。 |
boolean | offerLast(E e) | 在集合的末尾添加一个指定元素,返回一定为true。 |
E | peek() | 返回集合中的第一个元素,集合为空则返回null。 |
E | peekFirst() | 返回集合中的第一个元素,集合为空则返回null。 |
E | peekLast() | 返回集合中的第一个元素,集合为空则返回null。 |
E | poll() | 删除集合中的第一个元素,并返回该元素,集合为空则返回null。 |
E | pollFirst() | 删除集合中的第一个元素,并返回该元素,集合为空则返回null。 |
E | pollLast() | 删除集合中的最后一个元素,并返回该元素,集合为空则返回null。 |
E | pop() | 删除集合中的第一个元素,并返回该元素,集合为空则抛出异常。 |
void | push(E e) | 在集合的开头添加一个指定元素。 |
E | remove() | 删除集合中的第一个元素,并返回该元素,集合为空则抛出异常。 |
E | removeFirst() | 删除集合中的第一个元素,并返回该元素,集合为空则抛出异常。 |
E | removeLast() | 删除集合中的最后一个元素,并返回该元素,集合为空则抛出异常。 |
LinkedList 的常用操作可以参考上述的 ArrayList,此外它双向链表结构的特点可以配合上述的新添的方法达到模拟队列和栈的结构。
使用案例:
package work.java.xzk10301003;
import java.util.LinkedList;
/**
*@ClassName: LinkedListTest
*@Description: LinkedList的使用,模拟队列和栈
*
*/
public class LinkedListTest {
public static void main(String[] args) {
// 使用addFirst和removeLast组合模拟单端队列,或者使用addLast和removeFirst组合
LinkedList<Integer> queue = new LinkedList<>();
System.out.println("假设队列长度为3:");
queue.addFirst(1);
System.out.println("1入队列:");
System.out.println(queue);
queue.addFirst(2);
System.out.println("2入队列:");
System.out.println(queue);
queue.addFirst(3);
System.out.println("3入队列:");
System.out.println(queue);
queue.addFirst(4);
queue.removeLast();
System.out.println("4入队列,1出队列:");
System.out.println(queue);
// 使用push方法入栈,pop方法弹栈来模拟栈结构
LinkedList<Integer> stack = new LinkedList<>();
System.out.println();
System.out.println("假设栈大小为2:");
stack.push(1);
System.out.println("1入栈:");
System.out.println(stack);
stack.push(2);
System.out.println("2入栈:");
System.out.println(stack);
stack.pop();
System.out.println("2弹栈:");
System.out.println(stack);
stack.pop();
System.out.println("1弹栈:");
System.out.println(stack);
}
}
3.2 Set接口
Set 接口是 Collection 的子接口,与 List 接口的不同在于:
- Set 接口里面的内容是不允许重复的。List接口里的内容允许重复。
- Set 接口里元素是无序的。List接口里元素是有序的。该顺序指的是存入以及取出顺序。
接口定义: public interface Set<E> extends Collection<E>
Set 接口并没有对 Collection 接口进行扩充,基本上还是与 Collection 接口保持一致。
接口常用方法参考上述 Collection 接口。
因为此接口没有 List 接口中定义 的 get ( int index ) 方法,所以无法使用循环进行输出。可以将其转化为数组或使用迭代器。
Set 接口中有两个常用的子类:HashSet、TreeSet。
3.2.1 HashSet
HashSet 类是 Set 接口的一个实现类,它存储的元素是不可重复的,且元素都是无序的。它的底层实现得到了 HashMap 的支持。此类的 iterator 方法返回的迭代器是快速失败的(具体参考下述 Iterator)。
类名: public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable
HashSet 是根据对象的哈希值来确定元素在集合中存储的位置的,因此具有良好的存取和查找性能。
HashSet 的元素无序以及不重复的特点与 hashCode 和 equals 方法息息相关。所以在存储自定义类型时一定要重写这两个方法。基本数据类型的包装类和 String 类型都已实现了两方法所以可以直接使用。
使用案例:
package work.java.xzk10301003;
import java.util.HashSet;
import java.util.Objects;
/**
*@ClassName: HashSetTest
*@Description: HashSet的使用
*
*/
public class HashSetTest {
static class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public static void main(String[] args) {
// 使用HashSet存储基本数据类型或String类型
HashSet<String> set = new HashSet<>();
System.out.println("向HashSet集合中添加String类型数据:");
set.add("锄禾日当午");
boolean b1 = set.add("");
boolean b2 = set.add("");//添加两个null值,由于HashSet不允许重复,所以第二个null添加失败
System.out.println("第一个null:" + b1 + "\t第二个null:" + b2);
set.add("汗滴禾下土");
set.add("谁知盘中餐");
set.add("粒粒皆辛苦");
// 打印结果的顺序与添加时的顺序不一致
System.out.println(set); //使用了对象的toString方法
// 使用HashSet存储自定义类型,注意一定要重写hashCode和equals方法
HashSet<Person> set2 = new HashSet<>();
System.out.println();
System.out.println("向HashSet集合中添加自定义类型数据Person:");
set2.add(new Person("Mike", 18));
// 添加第二个Person对象,使用hashCode方法计算出的哈希值一样,使用equals比较对象结果一样,判定为重复对象,存储失败
set2.add(new Person("Mike", 18));
set2.add(new Person("Mike", 19));
set2.add(new Person("Bob", 17));
// 使用<T> T[] toArray(T[] a)方法将HashSet集合转化为对象数组,此处泛型指定为Person类
Person[] person = set2.toArray(new Person[]{});
for (int i = 0; i < person.length; i++) {
// 循环遍历对象数组并打印结果,结果顺序与添加顺序不一致
System.out.println(person[i]);
}
}
}
3.2.2 TreeSet
TreeSet 的基本操作与 HashSet 一样,它里面的元素是不可重复的,不同于 HashSet 的是它存储的元素是有序的,然而这个有序不是指的存入和取出的顺序而是系统默认的自然顺数,也可以是自己定义的排序。TreeSet 的底层实现得到了 TreeMap 的支持。此类的 iterator 方法返回的迭代器是快速失败的(具体参考下述 Iterator)。
类名: public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable
不同于 HashSet 的不重复以及无序性的实现依赖于重写 hashCode 和 equals 方法,TreeSet 的不重复以及有序性需要实现 Comparable 接口才能做到,基本数据类型以及 String 类型均实现了此接口,所以可以直接使用 TreeSet 集合储存,当要储存自定义类型时一定使该类实现 Comparable 接口(否则程序会报错),然后重写 compareTo 方法定义排序规则。
使用案例:
package work.java.xzk10301003;
import java.util.TreeSet;
/**
*@ClassName: TreeSetTest
*@Description: TreeSet的使用
*
*/
public class TreeSetTest {
static class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Person o) {
/**该方法返回值有三种:负数,零,正数
* 负数:this < o
* 零:this = o,此时表示对象o为重复数据,不允许存入
* 正数:this > o
* 由于基本数据类型和String类型,date类型都拥有compareTo方法,所以在自定义规则进行比较时推荐使用此方法
* int类型可以之间相减即可
* 升序排列:this.compareTo(o)或者 this - o
* 降序排列:o.compareTo(this)或者 o - this
*/
// return Integer.valueOf(this.age).compareTo(Integer.valueOf(o.age));
// 上行代码的实现效果与下行代码一样,使用的是包装类的compareTo方法,所以说int类型直接相减即可
return this.age - o.age;
}
}
public static void main(String[] args) {
// String类型已实现了Comparable接口,存入的字符串会按自然顺序排序
TreeSet<String> treeSet = new TreeSet<>();
System.out.println("添加String类型数据a,a,c,d,b,e:");
treeSet.add("a");
treeSet.add("a"); //相同字符串通过compareTo比较结果为0,表示存入数据相同,则存入失败
treeSet.add("c");
treeSet.add("d");
treeSet.add("b");
treeSet.add("e");
System.out.println(treeSet);
System.out.println();
// 向TreeSet集合中存入自定义类型,一定使自定义类实现Comparable接口,并重写compareTo方法设定排序规则
TreeSet<Person> treeSet2 = new TreeSet<>();
// 这里按照年龄age进行升序排列,一样年龄视为重复数据,存入失败
treeSet2.add(new Person("Bob", 18));
treeSet2.add(new Person("Mark", 17));
treeSet2.add(new Person("Bill", 19));
treeSet2.add(new Person("Han", 16));
treeSet2.add(new Person("Jack", 16)); //age重复,存入失败
Person[] people = treeSet2.toArray(new Person[]{}); //使用toArray方法将集合转换为对象数组
System.out.println("按照年龄升序排列:");
for (int i = 0; i < people.length; i++) {
System.out.println(people[i]);
}
}
}
3.2.3 HashSet和TreeSet的区别
区别 | HashSet | TreeSet |
---|---|---|
结构 | 使用哈希表结构存储 | 使用二叉树结构存储 |
特点 | 元素不可重复,元素无序 | 元素不可重复,元素有序(自然顺序或自定义顺序) |
实现依赖 | HashMap | TreeMap |
方法依赖 | hashCode和equals方法,在自定义类中必须要重写这两方法才能保证集合的不可重复和无序特点,如果没有重写,程序依然可以执行,但是结果将不符合预期,将失去使用HashSet的意义。 | compareTo方法,存储的数据类必须实现Comparable接口,然后重写compareTo方法,对于自定义类型必需要实现Comparable接口,否则程序运行会报错。 |
4 Iterator接口
Iterator 又称为迭代器,在 Java 中 Collection 和 Map 接口用于存储数据,而 Iterator 接口主要用于迭代访问,即遍历集合中的数据。
接口定义: public interface Iterator<E>
主要方法:
返回类型 | 方法名 | 描述 |
---|---|---|
boolean | hasNext () | 如果使用next能够返回一个元素而不是抛出异常则返回true,否则返回false。即游标的后面存在元素则返回true,不存在则返回false。 |
E | next () | 返回迭代器游标后面的一个元素E,并把游标后移一位。 |
void | remove () | 从集合中删除使用迭代器最后返回的一个元素。 |
实现原理:
要想使用 Iterator 接口,则必须使用 Collection 接口(子类也行),在 Collection 接口中有一个 iterator() 方法,可以使用该方法为 Iterator 接口进行实例化操作,这将获得一个该容器的迭代器,然后一直循环使用 hasNext 方法判断迭代器游标的后面是否有元素,有则使用 next 方法返回游标后面的元素并把游标后移一位,没有则退出循环表示迭代完毕。注意,刚获得迭代器时,游标在集合的最前面,即第一个元素的前面。
迭代器的快速失败和安全失败:
fail-fast快速失败:
在使用迭代器进行遍历操作时,集合发生了结构性修改(添加或删除了一个或多个元素,打乱元素的顺序,修改元素不算结构性修改),这将可能导致产生一些未知的错误,为了避免这种问题,在发生结构性修改时立即抛出 ConcurrentModificationException 异常来终止程序的运行。
fail-safe安全失败:
迭代器进行迭代时并不是在原集合上进行,而是在原集合的副本上进行迭代,此时即使原集合发生了结构性修改也不影响迭代器的遍历操作。
如何判断是否发生结构性修改?
每个集合中都有一个名为 modCount 的内部标识,每当集合发生结构性修改时都会更新 modCount 的值,而迭代器每一次迭代时都会检查这个值,一旦发生更改则会触发快速失败机制(只针对快速失败的迭代器)。
拥有快速失败的迭代器的集合:ArrayList、Vector、LinkedList、HashSet、TreeSet、HashMap、TreeMap
拥有安全失败的迭代器的集合:ConcurrentHashMap
使用案例:
package work.java.xzk10301003;
import java.util.ArrayList;
import java.util.Iterator;
/**
*@ClassName: IteratorTest
*@Description: Iterator的使用
*
*/
public class IteratorTest {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("c");
arrayList.add("f");
arrayList.add("d");
arrayList.add("s");
System.out.println("正常迭代遍历集合:");
// 使用iterator方法获得迭代器实例
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()) {
// 返回游标后面的元素并打印,然后游标后移
System.out.println(iterator.next());
}
System.out.println("在迭代时修改集合中指定元素:");
// 每一次新的迭代都需要获取新的迭代器,多个迭代器间互不干扰
Iterator<String> iterator2 = arrayList.iterator();
while (iterator2.hasNext()) {
// 返回游标后面的元素并打印,然后游标后移
System.out.println(iterator2.next());
// 修改集合元素不算是集合的结构性修改,不会触发快速失败机制
arrayList.set(2, "q");
}
System.out.println("在迭代时向集合中添加一个元素:");
Iterator<String> iterator3 = arrayList.iterator();
while (iterator3.hasNext()) {
// 返回游标后面的元素并打印,然后游标后移
System.out.println(iterator3.next());
// 添加元素到集合算结构性修改,modCount会加一,迭代时触发快速失败机制
arrayList.add("q");
}
}
}
4.1 ListIterator接口
ListIterator 是 Iterator 的子类,不用于 Iterator 的往下遍历集合,ListIterator 可以往上遍历集合。
接口定义: public interface ListIterator<E> extends Iterator<E>
新添方法:
返回类型 | 方法名 | 描述 |
---|---|---|
void | add (E e) | 将指定的元素插入到迭代器游标所在的位置,此时游标应在新元素的后面。 |
boolean | hasPrevious () | 如果使用previous能够返回一个元素而不是抛出异常则返回true,否则返回false。即游标的前面存在元素则返回true,不存在则返回false。 |
int | nextIndex () | 返回next将要返回的元素的索引,即游标后面的一个元素的索引。 |
E | previous () | 返回迭代器游标前面的一个元素,并把游标前移一位。 |
int | previousIndex () | 返回previous将要返回的元素的索引,即游标前面的一个元素的索引。 |
void | set (E e) | 用指定的元素替换迭代器使用next或previous最后返回的元素 |
使用案例:
package work.java.xzk10301003;
import java.util.ArrayList;
import java.util.ListIterator;
/**
*@ClassName: ListIteratorTest
*@Description: ListIterator的使用
*
*/
public class ListIteratorTest {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
arrayList.add("e");
// 使用listIterator方法获得迭代器实例
ListIterator<String> listIterator = arrayList.listIterator();
/**
* 个人理解:
* 迭代器结构:^ 0_a ^ 1_b ^ 2_c ^ 3_d ^ 4_e ^
* 其中 ^ 表示游标可能存在的位置,刚获得迭代器实例时,游标一定在第一个 ^ 所在的位置
* 其中 0_a、1_b、2_c等这些表示一个个单独的元素存储空间,比如0_a,0表示索引,a表示具体元素,_无意义仅链接索引和元素
* next作用:返回游标后的元素,游标后移一位
* nextIndex作用:返回游标后元素的索引
* previous作用:返回游标前的元素,游标前移一位
* previousIndex租用:返回游标前元素的索引
* 迭代器初始状态:^ 0_a 1_b 2_c 3_d 4_e
*/
System.out.println("ArrayList:" + arrayList);
// 返回:游标后索引0 游标:不动 打印:索引0 状态:^ 0_a 1_b 2_c 3_d 4_e
System.out.println(listIterator.nextIndex());
// 返回:游标后元素a 游标:后移一位 打印:元素a 状态: 0_a ^ 1_b 2_c 3_d 4_e
System.out.println(listIterator.next());
// 返回:游标后索引1 游标:不动 打印:索引1 状态: 0_a ^ 1_b 2_c 3_d 4_e
System.out.println(listIterator.nextIndex());
// 迭代器最后返回的元素是a,则将a修改为aa 状态: 0_aa ^ 1_b 2_c 3_d 4_e
listIterator.set("aa");
// 返回:游标后元素b 游标:后移一位 打印:元素b 状态: 0_aa 1_b ^ 2_c 3_d 4_e
System.out.println(listIterator.next());
// 返回:游标后元素c 游标:后移一位 状态: 0_aa 1_b 2_c ^ 3_d 4_e
listIterator.next();
// 返回:游标后元素d 游标:后移一位 状态: 0_aa 1_b 2_c 3_d ^ 4_e
listIterator.next();
// 返回:游标后索引4 游标:不动 打印:索引4 状态: 0_aa 1_b 2_c 3_d ^ 4_e
System.out.println(listIterator.nextIndex());
// 返回:游标前元素d 游标:前移一位 打印:元素d 状态: 0_aa 1_b 2_c ^ 3_d 4_e
System.out.println(listIterator.previous());
// 返回:游标前索引2 游标:不动 打印:索引2 状态: 0_aa 1_b 2_c ^ 3_d 4_e
System.out.println(listIterator.previousIndex());
// 迭代器最后返回的元素是d,则将d修改为dd 状态: 0_aa 1_b 2_c ^ 3_dd 4_e
listIterator.set("dd");
// 返回:游标前元素c 游标:前移一位 打印:元素c 状态: 0_aa 1_b ^ 2_c 3_dd 4_e
System.out.println(listIterator.previous());
// 删除迭代器最后返回的元素c 状态: 0_aa 1_b ^ 2_dd 3_e
listIterator.remove();
// 返回:游标前索引1 游标:不动 打印:索引1 状态: 0_aa 1_b ^ 2_dd 3_e
System.out.println(listIterator.previousIndex());
// 在游标处插入新元素cc,游标应处于新元素的后面 状态: 0_aa 1_b 2_cc ^ 3_dd 4_e
listIterator.add("cc");
// 返回:游标后索引3 游标:不动 打印:索引3 状态: 0_aa 1_b 2_cc ^ 3_dd 4_e
System.out.println(listIterator.nextIndex());
System.out.println("ArrayList:" + arrayList);
}
}
4.2 foreach
增强for循环,专门用于遍历数组和集合(Collection),其内部原理其实是个 Iterator 迭代器,它仅用于遍历,遍历过程中无法对数组或集合进行增删操作。
使用格式:
for(元素的数据类型 变量名 : Collection集合or数组){
//写操作代码
}
使用案例:
package work.java.xzk10301003;
import java.util.ArrayList;
/**
*@ClassName: ForeachTest
*@Description: 增强for循环的使用
*
*/
public class ForeachTest {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
// 增强for循环,对数组进行遍历实际上使用的就是普通的for循环
for (int i : arr) {
System.out.print(i+" ");
}
System.out.println();
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
arrayList.add("e");
// 增强for循环,对集合进行遍历实际上使用的是迭代器
for (String s : arrayList) {
System.out.print(s+" ");
}
}
}
5 Map接口
Map 以键值对的形式一次存储两个相映射的对象,一个键对象 key 映射一个值对象 value ,Map 中不允许出现重复的键。
接口定义: public interface Map<K,V>
常用方法:
补充:在使用 put 方法添加键值对时,如果添加的键在 Map 中已存在则会把 Map 中原来的键映射到新添加的值,并把原来的值返回。
5.1 HashMap
HashMap 是基于哈希表的 Map 接口的实现。此类实现了所有可选的映射操作。HashMap 允许存储 null 值和 null 键,但只能有一个null键,它存储的元素是无序的,且不保证元素的顺序随时间推移保持不变。
5.1.1 HashMap基于哈希表的存储机制
哈希表又称散列表,很多描述里的支持散列既是实现了 hashCode 方法,能够计算对象的哈希值,使用哈希表进行存储,拥有非常好的存取和查找性能。
hashCode方法:基于对象和哈希算法计算出一个 int 类型的哈希值,并将其返回。
什么是哈希表?
在 JDK1.8 之前,哈希表底层采用数组+链表实现。一个对象要存进哈希表会先计算其哈希值,然后用哈希值对哈希表的数组长度进行取余,得到的余数既是要存入的数组的下标。数组每一个下标表示的存储单元都是一个哈希桶,桶里存放的都是一个链表。当出现哈希值冲突的情况,即多个对象的哈希值取余结果相同,这时可以将冲突对象都放到该哈希桶的链表中。但是当位于一个桶中的元素较多时在链表中通过key值依次查找的效率也可能很低。
而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(大于或等于8)时,将链表转换为红黑树,因为在数据较多时红黑树的查找效率要高于链表,这样大大减少了查找时间。而当桶中元素个数从 8 个减少到 6 个时,将红黑树转换为链表。因为节点个数较少时两者的查找效率差不多,而链表占用的空间比红黑树更少,这样能节省空间。
在创建一个 HashMap 时初始的默认容量是 16 ,即有 16 个哈希桶,其实这一操作是在第一次添加数据时执行的,添加数据时会判断哈希表的长度是否为0,是的话将原表变成一个长度为 16 的新的哈希表用于存储。
负载因子: 每个哈希表都有负载因子,这个值可以在构造哈希表时指定,无指定则默认为 0.75 ,当哈希表中有 75% 的哈希桶被使用时,为了提高效率,HashMap 将创建一个新的哈希表,该表的桶的数量是原来的 2 倍,然后将所有数据存放到新的哈希表中。
提高哈希表的使用效率: 随着 HashMap 的数据的不断添加,哈希表可能会不断地重新创建,散列,而这会造成系统的消耗降低程序的效率,所以当要存储大量数据时指定合适的初始容量可以提高效率。默认负载因子 0.75 是经过试验测试出来的一个在时间和空间消耗上都比较合适的值,一般而言使用 0.75 就可以了,除非有特殊的条件,如时间或空间上的限制,但也要设置的尽可能合理。
5.1.2 HashMap的使用
HashMap 的特点与 HashSet 比较像,它们都是基于哈希表实现的,实际上 HashSet 的实现就有 HashMap 的支持。HashMap 在存数据时放入的是一对键值对,而HashSet 存数据时存进了一个对象元素,这个元素其实就存在 HashMap 的键中,这个键映射的值是一个新创建出来的 Object 对象。
由于使用到了哈希表,所以在使用自定义类当作键进行存储时一定要重写 hashCode 和 equals 方法,hashCode 用于找到存储的桶,equals 用于判断该键是否重复。同时轻易不要修改自定义类中参与了哈希值计算的变量的值,类的变化可能导致哈希值的变化,在查找数据时会根据新的哈希值查找,而数据存放在原来的哈希值计算的桶中,所以可能会出现查找不到数据,无法使用,无法修改,无法删除的情况,这将导致内存泄露。
使用案例:
package work.java.xzk10301003;
import java.util.HashMap;
import java.util.Objects;
import java.util.Set;
/**
*@ClassName: HashMapTest
*@Description: HashMap的使用
*
*/
public class HashMapTest {
static class Book {
private String title;
private String author;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(title, book.title) &&
Objects.equals(author, book.author);
}
@Override
public int hashCode() {
return Objects.hash(title, author);
}
@Override
public String toString() {
return "Book{" +
"title='" + title + '\'' +
", author='" + author + '\'' +
'}';
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Book(String title, String author) {
this.title = title;
this.author = author;
}
}
public static void main(String[] args) {
HashMap<Book, String> map = new HashMap<>();
map.put(new Book("java", "Jack"), "第一本书");
map.put(new Book("PHP", "Jane"), "第二本书");
map.put(new Book("C++", "Bob"), "第三本书");
// 使用keySet方法获取map的键的Set集合
Set<Book> books = map.keySet();
// 遍历Set集合,然后使用get方法通过键获取值,如此起到了遍历HashMap的作用
for (Book b : books) {
// 键值对根据键的哈希值存放,所以取出顺序与存放顺序不一定相同
System.out.println(b + map.get(b));
}
System.out.println("添加新的键值对,且该键已存在:");
// 添加重复键会将map中此键映射到新的值,并返回原值
String s = map.put(new Book("C++", "Bob"), "第四本书");
System.out.println("重复建所映射原值:" + s);
for (Book b : books) {
System.out.println(b + map.get(b));
}
}
}
5.2 TreeMap
TreeMap 子类是允许 key 进行排序的操作子类,其本身在操作的时候将按照 key 进行排序,另外,key 中的内容可以为任意的对象,但是要求对象所在的类必须实现 Comparable 接口。 这与 TreeSet 一样,其实 TreeSet 在添加数据时就是将数据存放在 TreeMap 中,它们的存储结构都是红黑树。
TreeMap 的基本操作与 HashMap 一样只是使用效率没有 HashMap 高,只有在需要排序时才会使用 TreeMap。
使用案例:
package work.java.xzk10301003;
import java.util.Set;
import java.util.TreeMap;
/**
*@ClassName: TreeMapTest
*@Description: TreeMap的使用
*
*/
public class TreeMapTest {
static class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Person o) {
// 判断age是否一样
if (this.age == o.age)
// age一样则按name顺序排列
return this.name.compareTo(o.name);
// age不一样则按age升序排列
return this.age - o.age;
}
}
public static void main(String[] args) {
TreeMap<Person, String> map = new TreeMap<>();
map.put(new Person("Han", 18), "学生");
map.put(new Person("Sam", 17), "学生");
map.put(new Person("Eric", 40), "老师");
map.put(new Person("Max", 19), "学生");
map.put(new Person("Ann", 19), "学生");
// 使用keySet方法获取map的键的Set集合
Set<Person> people = map.keySet();
System.out.println("按照年龄升序排列,年龄一样按名字顺序排列:");
// 使用迭代器遍历Set集合,并通过get方法获取值
for (Person p : people) {
System.out.println(p + map.get(p));
}
}
}
5.3 Map几个子类的区别
Map 的子类:HashMap、TreeMap、HashTable、CurrentHashMap、LinkedHashMap。
这个五个子类的基本操作没有区别。
区别 | HashMap | TreeMap | HashTable | CurrentHashTable | LInkedHashMap |
---|---|---|---|---|---|
线程 | 线程不安全 | 线程不安全 | 线程安全 | 线程安全,采用分段锁机制 | 线程不安全 |
效率 | 效率最高 | 效率最低 | 效率低 | 效率比HashTable高 | 效率比HashMap低 |
特点 | 无序,不可重复,允许存在一个null键,多个null值 | 有序,自然排序或自定义顺序,键唯一,值可重复,不许存在null键 | 无序,不可重复,键和值都不能是null | 无序,不可重复,键和值都不能是null | 有序(存入顺序),不可重复,允许存在一个null键,多个null值 |
存储结构 | 哈希表 | 红黑树 | 哈希表 | 哈希表 | 哈希表和双向链表(仅用于存储顺序,遍历使用) |
实现要求 | 存储对象所在类必须重写hashCode和equals方法 | 存储的键对象所在类必须实现Comparable接口,并重写compareTo方法 | 存储对象所在类必须重写hashCode和equals方法 | 存储对象所在类必须重写hashCode和equals方法 | 存储对象所在类必须重写hashCode和equals方法 |
5.4 Map的遍历输出
方式一:
使用 keySet 方法获得键集合,然后遍历集合使用 get 方法获得键映射的值。
方式二:
使用 Map.Entry 接口,通过调用 entrySet 方法获取 Set 集合,然后遍历集合,然后分别使用该接口的 getKey 和 getValue 方法获取键和值。实际上 Map 集合中的每一个元素都是一个 Map.Entry 实例,该实例中保存着 key 和 value ,必须使用接口方法才能分别获取键值。
使用案例:
package work.java.xzk10301003;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
*@ClassName: MapTest
*@Description: Map集合的输出遍历
*
*/
public class MapTest {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("ten ", 10);
map.put("one ", 1);
map.put("six ", 6);
map.put("two ", 2);
map.put("four ", 4);
System.out.println("使用keySet遍历:");
Set<String> set = map.keySet();
for (String s : set) {
System.out.println(s + map.get(s));
}
System.out.println("使用entrySet遍历:");
// Map.Entry<K,V>就是一种数据类型,这里给泛型指定了类型
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
for (Map.Entry<String, Integer> m : entrySet) {
// getKey和getValue方法在Map.Entry接口中,只用使用Map接口的entrySet获得实例后才能使用
System.out.println(m.getKey() + m.getValue());
}
}
}
6 Collections工具类
此类仅包含对集合进行操作或返回集合的静态方法,是一个集合操作的工具类。
类名: public class Collections extends Object
部分常用方法:
public static <T> boolean addAll(Collection<T> c, T... elements)
:往集合中添加一些元素。public static void shuffle(List<?> list)
:打乱集合内元素的顺序。public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序
这里主要介绍使用 sort 方法利用比较器 Comparator 自定义规则进行排序。
使用案例:
package work.java.xzk10301003;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
*@ClassName: CollectionsTest
*@Description: Collections的sort方法排序
*
*/
public class CollectionsTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("ab");
list.add("da");
list.add("cc");
list.add("bd");
System.out.println("List集合初始顺序:");
System.out.println(list);
Collections.sort(list); //这里按自然顺序排序
System.out.println("sort排序后自然顺序排列:");
System.out.println(list);
// 传入比较器Comparator,自定义排序规则
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
// 按字符串的第二个字符升序
return o1.charAt(1) - o2.charAt(1);
}
});
System.out.println("sort比较器按元素第二个字符升序排列:");
System.out.println(list);
}
}
7 JDK9集合新特性
JDK9 提供了一些用于创建固定大小的集合的静态方法方法,该集合是无法进行任何修改的。
这些方法只存在于 List、Set、Map 这三个接口中,它们的子类里是没有的。
使用案例:
package work.java.xzk10301003;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
*@ClassName: OfTest
*@Description: JDK9新特性,固定集合静态方法of的使用
*
*/
public class OfTest {
public static void main(String[] args) {
List<String> list = List.of("一", "二", "三");
// list.add("s");
// list.remove("一");
// list.set(1,"a");
// 以上三个方法都不能使用,一旦使用就会抛出异常
System.out.println(list);
Set<String> set = Set.of("一", "二", "三");
System.out.println(list);
Map<String, Integer> map = Map.of("一", 1, "二", 2, "三", 3);
Set<String> keySet = map.keySet();
for (String s : keySet) {
System.out.println(s + "->" + map.get(s));
}
}
}