Java之集合
- Java提供了一套相当完整的容器类,其中基本的类型时List、Set、Queue和Map。
- Java容器的作用是保存对象,分为两种:
- ①Collection:一个独立元素的序列,这些元素都服从一条或多条规则。所有的Connection都可以for-each循环遍历。
- List:用来存储有序的元素序列。
- ArrayList:基于数组的实现,可以通过索引(下标)快速访问元素,但插入、删除元素时需要移动其他元素,效率较低。
- LinkedList:基于链表的实现,在插入、删除元素时只需要调整相邻元素的指针即可,效率较高,但访问元素时需要遍历整个链表,效率较低。
- Set:不能有重复的元素。
- HashSet:无序,使用哈希表来存储元素,具有很快的插入、删除和查找的速度。但是对于需要有序的操作如遍历,需要将元素拷贝到一个List集合中或使用TreeSet。
- TreeSet:有序,它按元素的自然顺序或Comparator的比较结果的升序排列。使用红黑树存储元素,查找、插入和删除的时间复杂的都是O(logn),它可以进行有序的遍历操作。
- LinkedHashSet:有序,它按照被添加的顺序保存对象。它继承自HashSet,内部使用链表维护元素的插入顺序。在遍历时,它可以按照插入的顺序进行迭代,同时还具有HashSet的高性能。
- Queue:按照排队规则来确定对象的顺序(与它们被插入的顺序相同)。
- PriorityQueue:基于优先级的队列,每个元素有一个优先级,优先级高的先出队。使用堆(Heap)实现,可以快速定位队列中的最大最小值,插入和删除的时间复杂度为O(logn)。
- List:用来存储有序的元素序列。
- ②Map:一组成对的“键值对”对象,允许使用键来查找值,就像字典一样。
- HashMap:无序,基于哈希表实现,但提供了最快的查找和插入技术。
- TreeMap:有序,它按照比较结果的升序保存键。基于红黑树实现。
- LinkedHashMap:它按照插入顺序保存键,基于哈希表和双向链表实现,保留了HashMap的查询速度。
- ①Collection:一个独立元素的序列,这些元素都服从一条或多条规则。所有的Connection都可以for-each循环遍历。
- Java集合提供了重写了toString()方法,因此直接打印即可。
List
ArrayList
- 使用:
List<Dog> dogs = new ArrayList<>();
。这里ArrayList向上转型为List。 - 尖括号括起来的是类型参数,可以有多个,它制定了这个容器实例可以保存的类型。
- 通过泛型,就可以在编译期防止将错误类型的对象放置到容器中。
- 当指定了泛型类型后,你可以将该类型的对象放入其中,当从列表中取出的对象的类型就是你指定的类型,就不用再进行类型转换了。
- 当指定了某个类型作为泛型参数时,并不仅限于只能将该类型的对象放置到容器中,向上转型也可以像作用于其他类型一样作用于泛型。
-
//动物 abstract class Animal {} //陆生动物 abstract class TerrestrialAnimal extends Animal {} //水生动物 abstract class AquaticAnimal extends Animal {} //两栖动物 abstract class AmphibiousAnimal extends Animal {} //鸟类 class Bird extends TerrestrialAnimal {} //犬类 class Dog extends TerrestrialAnimal {} //鱼类 class Fish extends AquaticAnimal {} //蛙类 class Frog extends AmphibiousAnimal {} //哈士奇 class Husky extends Dog {} //鲤鱼 class Carp extends Fish {} //鲨鱼 class Shark extends Fish {} class Test { public static void main(String[] args) { ArrayList<Animal> animals = new ArrayList<>(); animals.add(new Bird()); animals.add(new Dog()); animals.add(new Fish()); animals.add(new Frog()); animals.add(new Husky()); animals.add(new Carp()); animals.add(new Shark()); for (Animal animal : animals) { System.out.println(animal); } } } 输出: com.eos.javalearn.Bird@39ed3c8d com.eos.javalearn.Dog@71dac704 com.eos.javalearn.Fish@123772c4 com.eos.javalearn.Frog@2d363fb3
-
- java.util.Arrays.asList()方法接收一个数组或是一个用逗号分隔的元素列表(原理是使用可变参数…),并将其转换成一个List对象。
- 使用这种方法创建的List是不可以向其中添加元素或从中删除元素的,因为其底层使用的是数组。
-
int[] i = {1, 2, 3}; List<int[]> a = Arrays.asList(i); List<Integer> b = Arrays.asList(1, 2, 3); Integer[] i1 = {1, 2, 3}; List<Integer> c = Arrays.asList(i1); //会报错 //c.remove(1); //c.add(1); List<Integer> d = new ArrayList<>(); //没问题 d.add(1); d.remove(0);
- 如果Arrays.asList()中只有TerrestrialAnimal类型时,需要将List的泛型指定为TerrestrialAnimal,否则会报错,如果要解决这个问题,就需要进行显式类型参数说明。
- 其实就是找到这些类型公共的且离它们最近的父类型作为泛型的类型。
-
//会报错,因为它们最近的公共基类为TerrestrialAnimal,而不是Animal //List<Animal> animals1 = Arrays.asList(new Dog(), new Bird()); //没问题 List<TerrestrialAnimal> animals1 = Arrays.asList(new Dog(), new Bird()); //它们最近的公共基类为Animal List<Animal> animals2 = Arrays.asList(new Dog(), new Bird(), new Carp()); //显式类型参数说明 List<Animal> animals3 = Arrays.<Animal>asList(new Dog(), new Bird());
- Collections的addAll()方法接收一个Collection对象,以及一个数组或是一个用逗号分隔的元素列表,将元素添加到Collection中。
- 对于Arrays.asList()出现的问题,在这里就不会发生。因为它从第一个参数中已经知道了目标类型是什么。
-
List<Animal> animals4 = new ArrayList<>(); Collections.addAll(animals4, new Dog(), new Bird());
-
- 对于Arrays.asList()出现的问题,在这里就不会发生。因为它从第一个参数中已经知道了目标类型是什么。
- 除了以上两种初始化方式外,还可以使用双括号进行初始化:
- 这种方式需要使用两个花括号,第一个花括号用于创建一个匿名内部类来扩展List接口,第二个花括号用于添加初始元素。
- 注意:这种方式创建的列表是可变的。但是由于它创建了一个匿名内部类,因此可能会对性能产生影响。
-
ArrayList<String> strings = new ArrayList<String>() { { add("1"); add("2"); add("3"); } };
- Collections常用方法:
- 除了上面说的addAll()方法以外,还有一些其他常用的方法。
- sort():接收一个集合,对其进行排序。使用的是归并排序算法。这里并不关心指定集合中元素的顺序,只关心存不存在。
- shuffle():接收一个集合,用于随机打乱集合中元素的顺序。使用了伪随机算法。
- shuffle():接收一个集合和一个Random对象,用于随机打乱集合中元素的顺序。Random对象用来指定随机化算法。
- 除了上面说的addAll()方法以外,还有一些其他常用的方法。
- ArrayList常用方法:
- 增:
- add(Object):添加一个元素。
- add(int, Object):在指定的下标插入一个元素。
- addAll(int, Collection):从指定下标开始插入指定集合中的元素,原来的元素向后挪。
- 删:
- remove(Object):删除指定的元素。如果集合中不存在该元素不会有任何响应,如果存在多个则只会删除第一个,返回是否删除成功。
- remove(int):删除指定下标的元素,如果下标越界则会报数组越界异常,返回是否删除成功。
- removeAll(Collection):删除指定集合中的元素,如果某个元素存在多个则只会删除第一个。
- 改:
- set(int, Object):将指定下标的元素修改为新元素。
- 查:
- get(int):通过下标获取元素。
- binarySearch():使用二分法查找元素。
- 其他:
- unmodifiable系列方法:生成指定容器的只读版本,若对其进行修改操作会报错。
- synchronized系列方法:与同步相关。
- contains(Object):检查列表中是否包含指定的元素。
- containsAll(Collection):检查列表中是否包含指定集合中的所有元素,
- indexOf(Object):获取指定元素的下标。
- subList(int, int):从原集合中根据指定的下标区间获取元素并返回一个新的集合(即,获取子集合)。区间左闭右开,如subList(1, 4)表示区间[1, 4)的元素。
- retainAll(Collection):用于保留集合中与指定集合中相同的元素,即保留两个集合的交集。
- isEmpty():检查集合中的元素个数是否为0。
- clear():移除集合中所有的元素。
- toArray():将列表转换成一个对象数组。
- toArray(T[]):将集合中的元素转换成指定类型的数组,如果指定的数组容量小于集合的元素数量,那么会创建一个新的数组,新数组的大小就是集合元素的个数,如果指定的数组容量大于集合的元素数量,那么数组中的剩余元素将被设置为 null。如果指定类型的数组的类型与集合中元素的类型不能向上或向下转型就会报错,这和上面Arrays.asList()方法出现的错一样。
- 增:
- 注:这些方法或多或少会受到equals方法的影响,List可能会因为equals方法的行为不同而表现出不同行为。
LinkedList
- LinkedList在插入、删除元素时更高效一点,但是在随机访问上要差一些。
- LinkedList添加了将其用作栈(Stack)、队列(Queue)或双端队列(Deque)的方法。
Stack
- 栈是一种后进先出(LIFO)的容器。
- LinkedList具有能够直接实现栈的所有功能的方法。
- 它的操作方法包括(前4个是基本操作):
- push:向栈顶添加一个元素,即入栈。
- pop:从栈顶移除一个元素,并返回该元素的值,即出栈。
- top/peek:返回栈顶元素的值,但不移除该元素。
- isEmpty:判断栈是否为空。
- isFull:判断栈是否已满(对于固定大小的栈,有该方法)。
- size:返回栈中元素的数量。
- clear:清空栈中的所有元素。
- search:查找某个元素在栈中的位置,并返回距离栈顶的距离。如果元素不存在于栈中,返回-1。
- 由于java.util.Stack太老而且它设计的不太好,所以一般我们不用它,而是用LinkedList。同时,我们可以自己使用LinkedList封装一个栈:
-
class Stack<T> { private LinkedList<T> linkedList = new LinkedList<>(); public void push(T v) { linkedList.push(v); } public T pop() { return linkedList.pop(); } public T peek() { return linkedList.peek(); } public boolean isEmpty() { return linkedList.isEmpty(); } @Override public String toString() { return linkedList.toString(); } } 使用: Stack<Integer> stack = new Stack<>(); for (int i = 0; i < 5; i++) { stack.push(i); } System.out.println(stack); 输出: [4, 3, 2, 1, 0]
- 这里我们定义了一个可以持有T类型对象的Stack,在类被使用时,T将会被实际的类型替换。
-
Queue
- 队列是一种先进先出(FIFO)的容器。
- LinkedList具有能够直接实现队列的所有功能的方法。
- 常用方法:
- offer(E e):将元素插入到队列尾部,如果队列满了,返回false。
- add(E e):将元素插入到队列尾部,如果队列满了,抛出异常。
- poll():获取并删除队列头部元素,如果队列为空,则返回null。
- remove():获取并删除队列头部元素,如果队列为空,则抛出异常。
- peek():获取队列头部元素,但是不删除,如果队列为空,则返回null。
- element():获取队列头部元素,但是不删除,如果队列为空,则抛出异常。
- size():获取队列中元素的个数。
- isEmpty():判断队列是否为空。
-
Queue<Integer> queue = new LinkedList<>(); for (int i = 0; i < 5; i++) { if (i < 3) { queue.add(i); } else { queue.offer(i); } } System.out.println(queue); System.out.println("peek方法获取队头元素:" + queue.peek()); System.out.println("element方法获取队头元素:" + queue.element()); System.out.println("poll方法移除队头元素" + queue.poll()); System.out.println(queue); System.out.println("remove方法移除队头元素" + queue.remove()); System.out.println(queue); 输出: [0, 1, 2, 3, 4] peek方法获取队头元素:0 element方法获取队头元素:0 poll方法移除队头元素0 [1, 2, 3, 4] remove方法移除队头元素1 [2, 3, 4]
PriorityQueue
- PriorityQueue是一个优先队列。
- 操作方法:
- add(E e):添加一个元素到队列中。
- offer(E e):添加一个元素到队列中,如果队列已满,则返回false。
- remove():移除并返回队列头部的元素,如果队列为空,则抛出异常。
- poll():移除并返回队列头部的元素,如果队列为空,则返回null。
- element():返回队列头部的元素但不移除,如果队列为空,则抛出异常。
- peek():返回队列头部的元素但不移除,如果队列为空,则返回null。
- size():返回队列中元素的个数。
- isEmpty():判断队列是否为空。
-
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(); for (int i = 0; i < 10; i++) { Random random = new Random(); priorityQueue.add(random.nextInt(100) + 1); } System.out.println(priorityQueue); 输出: [1, 4, 32, 50, 63, 71, 51, 86, 91, 72]
Deque
- 双端队列是一种具有队列和栈的特性的数据结构,可以在队列两端进行插入和删除。
- 常用方法:
- addFirst(E e):在队列头部插入元素。
- addLast(E e):在队列尾部插入元素。
- offerFirst(E e):在队列头部插入元素,如果队列满了,返回false。
- offerLast(E e):在队列尾部插入元素,如果队列满了,返回false。
- removeFirst():删除并获取队列头部元素,如果队列为空,则抛出异常。
- removeLast():删除并获取队列尾部元素,如果队列为空,则抛出异常。
- pollFirst():删除并获取队列头部元素,如果队列为空,则返回null。
- pollLast():删除并获取队列尾部元素,如果队列为空,则返回null。
- getFirst():获取队列头部元素,但是不删除,如果队列为空,则抛出异常。
- getLast():获取队列尾部元素,但是不删除,如果队列为空,则抛出异常。
- peekFirst():获取队列头部元素,但是不删除,如果队列为空,则返回null。
- peekLast():获取队列尾部元素,但是不删除,如果队列为空,则返回null。
- size():获取队列中元素的个数。
- isEmpty():判断队列是否为空。
Set
- Set不保存重复的元素。
- 主要包括HashSet、TreeSet、LinkedHashSet。
- HashSet:为了快速查找而设计的Set,放入其中的元素必须定义hashCode()。
- TreeSet:保持次序的Set,底层为树结构。使用它可以从Set中提取有序的序列,放入其中的元素必须实现Comparable接口。
- LinkedHashSet:具有HashSet的查询速度,内部使用链表维护元素的顺序。按插入的顺序排列。放入其中的元素也必须定义hashCode()方法。
- 常用方法:
- add(Object):向Set中添加元素。
- addAll(Set):向Set中添加指定集合中的元素。
- contains(Object):检查Set中是否包含指定元素。
- containsAll(Set):检查Set中是否包含指定Set中的元素。
- remove(Object):从Set中移除指定元素。
- removeAll(Set):从Set中移除指定Set中的元素。
-
Set<Integer> set = new HashSet(); for (int i = 0; i < 5; i++) { set.add(i); } System.out.println(set); set.addAll(Arrays.asList(100, 200, 300)); System.out.println(set); System.out.println("集合中是否包含100:" + set.contains(400)); boolean is = set.containsAll(Arrays.asList(100, 200, 300)); System.out.println("集合中是否包含[100, 200, 300]:" + is); set.remove(2); System.out.println(set); set.removeAll(Arrays.asList(100, 200, 300)); System.out.println(set); 输出: [0, 1, 2, 3, 4] [0, 1, 2, 3, 4, 100, 200, 300] 集合中是否包含100:false 集合中是否包含[100, 200, 300]:true [0, 1, 3, 4, 100, 200, 300] [0, 1, 3, 4]
SortedSet
- 常用方法:
- add(E e) :添加元素e到集合中。
- remove(Object o) :从集合中删除元素o。
- size() :返回集合中元素的数量。
- clear() :清空集合中的所有元素。
- contains(Object o) :判断集合中是否包含元素o。
- first() :返回集合中的第一个元素。
- last() :返回集合中的最后一个元素。
- subSet(E fromElement, E toElement) :返回集合中从fromElement到 toElement之间的元素子集。
- headSet(E toElement) :返回集合中小于toElement的元素子集。
- tailSet(E fromElement) :返回集合中大于等于fromElement的元素子集。
-
SortedSet<Integer> sortedSet = new TreeSet<>(Arrays.asList(1, 2, 3, 4, 5, 6)); System.out.println("所有的元素为:" + sortedSet); System.out.println("第一个元素为:" + sortedSet.first()); System.out.println("最后一个元素为:" + sortedSet.last()); System.out.println("子集元素为:" + sortedSet.subSet(0, 4)); System.out.println("元素为:" + sortedSet.headSet(3)); System.out.println("元素为:" + sortedSet.tailSet(2)); 输出: 所有的元素为:[1, 2, 3, 4, 5, 6] 第一个元素为:1 最后一个元素为:6 子集元素为:[1, 2, 3] 元素为:[1, 2] 元素为:[2, 3, 4, 5, 6]
BitSet
- 如果想要高效率地存储大量“开/关”信息,可以使用它。当然这个效率只是空间上的,在访问上它比数组慢一点。
- 同时也可以选择EnumSet来代替它,因为EnumSet可以按名字进行操作,更加直观。
- 使用BitSet而不是EnumSet的理由只包括:
- ①只有在运行时才知道需要多少个标志位;
- ②对标志命名不合理;
- ③需要BitSet中的某种特殊操作。
- 使用BitSet而不是EnumSet的理由只包括:
Map
- 由于需要根据键来查找值,所以键值必须唯一,但是值可以重复。
- 主要分为:
- HashMap:使用哈希表实现。它可以接受null键和null值,并且不保证有序。插入和查询键值对的开销是固定的,可以通过构造器设置容量和负载因子。
- TreeMap:基于红黑树实现。查看键或键值对时,它们会被排序,次序由Comparable或Comparator决定。TreeMap是唯一带有subMap()方法的map,返回一个子树。
- LinkedHashMap:使用双向链表和哈希表实现。它保证插入元素的顺序。比HashMap慢一点,而在迭代访问时更快。
- Hashtable:使用哈希表实现。它不支持null键和null值,并且不保证有序。
- ConcurrentHashMap:使用哈希表实现。它是线程安全的HashMap,支持并发的读和写操作。
- WeakHashMap:使用哈希表实现。它的键是弱引用,当键没有被引用时,可以被自动清除。
- IdentityHashMap:使用哈希表实现。它是一种特殊的Map,比较键的时候不使用equals()方法,而是使用==运算符。
- 常用方法:
- put(Object, Object):向Map中添加一个元素键值对。
- get(Object):通过键查找值。
- containsKey(Object):查询Map中是否包含指定的键。
- containsValue(Object):查询Map中是否包含指定的值。
-
Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < 5; i++) { map.put(i, i); } System.out.println(map); System.out.println("键1对应的值为:" + map.get(1)); System.out.println("是否包含键9:" + map.containsKey(9)); System.out.println("是否包含值2:" + map.containsValue(2)); 输出: {0=0, 1=1, 2=2, 3=3, 4=4} 键1对应的值为:1 是否包含键9:false 是否包含值2:true
- Map的键和值还可以设置为其他Collection,因此可扩展的用法很多。
-
Map<Dog, List<String>> dogListMap = new HashMap<>(); Dog dog1 = new Dog("lala"); List<String> names1 = new ArrayList<>(Arrays.asList("la", "xiao la")); Dog dog2 = new Dog("wangwang"); List<String> names2 = new ArrayList<>(Arrays.asList("wang", "xiao wang")); dogListMap.put(dog1, names1); dogListMap.put(dog2, names2); System.out.println(dogListMap); Set<Dog> dogs = dogListMap.keySet(); System.out.println(dogs); Collection<List<String>> values = dogListMap.values(); System.out.println(values); 输出: {lala=[la, xiao la], wangwang=[wang, xiao wang]} [lala, wangwang] [[la, xiao la], [wang, xiao wang]]
-
SortedMap
- TreeMap是其实现。
- 常用方法:
- comparator():返回排序使用的比较器;如果该SortedMap使用自然顺序,则返回null。
- firstKey():返回第一个(最小的)key。
- lastKey():返回最后一个(最大的)key。
- subMap(K fromKey, K toKey):返回键从fromKey(包括)到toKey(不包括)的部分视图(即子SortedMap)。
- headMap(K toKey):返回键小于toKey的部分视图(即子SortedMap)。
- tailMap(K fromKey):返回键大于等于fromKey的部分视图(即子SortedMap)。
- get(Object key):根据key获取对应value。
- put(K key, V value):将key-value键值对加入SortedMap。
- remove(Object key):根据key,删除对应的key-value键值对。
- size():返回SortedMap中键值对的个数。
- containsKey(Object key):判断SortedMap中是否包含对应的key。
- containsValue(Object value):判断SortedMap中是否包含对应的value。
- clear():清空SortedMap中所有的键值对。
LinkedHashMap
- 虽然它也会散列化所有元素,但是在遍历键值对时,却会以元素的插入顺序返回键值对。
- 可以在构造器中指定使用最近最少使用算法LRU,没有被访问的元素就会出现在队列的前面。
-
LinkedHashMap<Integer, Integer> linkedHashMap = new LinkedHashMap<>(); for (int i = 0; i < 5; i++) { linkedHashMap.put(i, i); } System.out.println("所有元素为:" + linkedHashMap); linkedHashMap = new LinkedHashMap<>(10, 0.75f, true); for (int i = 0; i < 5; i++) { linkedHashMap.put(i, i); } System.out.println("使用LRU算法,所有元素为:" + linkedHashMap); for (int i = 0; i < 3; i++) { linkedHashMap.get(i); } System.out.println("访问元素后,所有元素为:" + linkedHashMap); linkedHashMap.get(0); System.out.println("访问元素0后,所有元素为:" + linkedHashMap); 输出: 所有元素为:{0=0, 1=1, 2=2, 3=3, 4=4} 使用LRU算法,所有元素为:{0=0, 1=1, 2=2, 3=3, 4=4} 访问元素后,所有元素为:{3=3, 4=4, 0=0, 1=1, 2=2} 访问元素0后,所有元素为:{3=3, 4=4, 1=1, 2=2, 0=0}
WeakHashMap
- 它是Java中的一种Map数据结构。
- 特点:使用弱引用来引用Map中的key。
- 与HashMap不同的是,当某个key对象没有任何强引用时,它可以被垃圾回收器回收,这意味着WeakHashMap中的key可能被自动删除,因此在使用时需要格外小心。
- 使用WeakHashMap的key一般是一些缓存数据的引用,因为不使用的缓存数据很可能会被垃圾回收。WeakHashMap的key可以被垃圾回收,但它并不是立即被回收的,而是被带有引用队列的ReferenceQueue监控,当垃圾回收器发现key被回收时,将自动把key-value对移除出Map,这样就避免了使用强引用带来的内存泄漏问题。
- 注:当使用WeakHashMap时,如果一个key-value对中的key对象被回收了,但value对象仍然被其他地方所引用,那么这个key-value对并不会被自动移除,它们仍然会留在WeakHashMap中,直到value对象也被回收掉才会被删除。所以,使用WeakHashMap时需要特别注意这个问题。
散列
- 为了使与哈希相关的集合能正常工作,通常我们需要重写存入其中的对象的hashCode()和equals()方法。具体可以去查看如HashMap的源码,其中会用到这两个方法。
- equals():
- 默认是比较对象的地址。
- 正确的重写需满足以下特点:
- ①自反性:对于任何非null的引用值x,x.equals(x)必须返回true。
- ②对称性:对于任何非null的引用值x和y,如果x.equals(y)返回true,则y.equals(x)也必须返回true。
- ③传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,则x.equals(z)也必须返回true。
- ④一致性:对于任何非null的引用值x和y,反复调用x.equals(y)始终返回true或始终返回false,前提是对象上equals比较中用到的信息没有被修改。
- ⑤对于任何非null的引用值x,x.equals(null)必须返回false。
-
class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (o == null) return false; if (this == o) return true; if (!(o instanceof Person)) return false; Person person = (Person) o; if (age != person.age) return false; return name.equals(person.name); } }
- hashCode():
- 默认是根据对象的地址计算散列码。
- 使用散列的目的:想要使用一个对象来查找另一个对象。
- 散列的价值在于速度:能使得查询快速进行。
- 为了速度,散列将键的信息保存在一个数组中,这个信息就是通过键对象的散列函数生成一个散列码,然后将散列码根据某种规则变成数组的下标,只要我们使用键对象就能查找到键在哪个数组的下标中,然后再取出下标中的List,最后根据键从List中取出键值对。
- 总结来看:可以看出散列表的结构,一个数组用于存放key的信息,只要我们有key,就能快速找到它在数组的哪个下标中,我们再在想·对应的下标中去查找键值对,这样就不用遍历整个数组去查找键值对了。这也就是散列快的原因了。数组元素如果存的是List,那在查找这个List时还是得线性遍历的查找。
- 散列表中的槽位通常叫做桶位(bucket)。
- 容量:表中的桶位数。
- 初始容量:表在创建时所拥有的桶位数。HashSet和HashMap有可以指定初始容量的构造方法。
- 尺寸:表中当前存储的项数。
- 负载因子:尺寸/容量。空表的负载因子是0,半满表的负载因子是0.5。HashSet和HashMap有可以指定负载因子的构造方法,当负载情况达到负载因子的水平时,容器将自动增加容量(自动扩容)。扩容的实现方式是使容量大致加倍,并重新将现有的对象分布到新的桶位集中(这个过程也被称为再散列)。HashMap默认的负载因子是0.75。
-
class MapEntry<K, V> implements Map.Entry<K, V> { private K key; private V value; public MapEntry(K key, V value) { this.key = key; this.value = value; } @Override public K getKey() { return key; } @Override public V getValue() { return value; } @Override public V setValue(V v) { V res = value; value = v; return res; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MapEntry<?, ?> mapEntry = (MapEntry<?, ?>) o; return Objects.equals(key, mapEntry.key) && Objects.equals(value, mapEntry.value); } @Override public int hashCode() { return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); } @Override public String toString() { return key + "=" + value; } } class SimpleHashMap<K, V> extends AbstractMap<K, V> { static final int SIZE = 997; LinkedList<MapEntry<K,V>>[] buckets = new LinkedList[SIZE]; public V put(K key, V value) { V oldV = null; int index = Math.abs(key.hashCode()) % SIZE; if (buckets[index] == null) { buckets[index] = new LinkedList<>(); } LinkedList<MapEntry<K, V>> bucket = buckets[index]; MapEntry<K, V> pair = new MapEntry<>(key, value); boolean found = false; ListIterator<MapEntry<K, V>> iterator = bucket.listIterator(); while (iterator.hasNext()) { MapEntry<K,V> iPair = iterator.next(); if (iPair.getKey().equals(key)) { oldV = iPair.getValue(); iterator.set(pair); found = true; break; } } if (!found) { buckets[index].add(pair); } return oldV; } public V get(Object key) { int index = Math.abs(key.hashCode()) % SIZE; if (buckets[index] == null) { return null; } for (MapEntry<K, V> iPair : buckets[index]) { if (iPair.getKey().equals(key)) { return iPair.getValue(); } } return null; } @Override public Set<Entry<K, V>> entrySet() { Set<Map.Entry<K,V>> set = new HashSet<>(); for (LinkedList<MapEntry<K,V>> bucket : buckets) { if (bucket == null) { continue; } for (MapEntry<K,V> mPair : bucket) { set.add(mPair); } } return set; } public static void main(String[] args) { SimpleHashMap<String, String> simpleHashMap = new SimpleHashMap<>(); simpleHashMap.put("1", "a"); simpleHashMap.put("2", "b"); simpleHashMap.put("3", "c"); simpleHashMap.put("4", "d"); System.out.println(simpleHashMap); System.out.println(simpleHashMap.get("1")); System.out.println(simpleHashMap.entrySet()); } } 输出: {1=a, 2=b, 3=c, 4=d} a [1=a, 2=b, 3=c, 4=d]
- 重写hashCode()的规范:
- ①给int赋值非零常量,如17;
- ②为对象每个有意义的字段(就是可以做equals()操作的字段)计算一个int散列码;
- ③合并计算得到散列码;
- ④返回最终的散列码;
- ⑤ 检查相同对象的散列码是否相同。
-
字段类型 计算 boolean c = (f ? 0 : 1) byte、char、short或int c = (int) f long c = (int) (f ^ (f>>>32)) float c = Float.floatToIntBits(f) double long l = Double.doubleToLongBits(f); c = (int) (l ^ (l>>>32)) Object c = f.hashCode() 数组 对每个元素使用上述规则 -
class StringCount { private static List<String> strings = new ArrayList<>(); private String s; private int count; public StringCount(String s) { this.s = s; strings.add(s); for (String s1 : strings) { if (s1.equals(s)) { count++; } } } @Override public String toString() { return "String: " + s + ", count: " + count + ", 哈希码:" + hashCode(); } @Override public int hashCode() { int result = 17; result = 37 * result + s.hashCode(); result = 37 * result + count; return result; } @Override public boolean equals(Object obj) { return obj instanceof StringCount && s.equals(((StringCount) obj).s) && count == (((StringCount) obj).count); } public static void main(String[] args) { Map<StringCount, Integer> map = new HashMap<>(); StringCount[] stringCounts = new StringCount[5]; for (int i = 0; i < stringCounts.length; i++) { stringCounts[i] = new StringCount("a"); map.put(stringCounts[i], i); } System.out.println(map); for (StringCount s : stringCounts) { System.out.println("StringCount = " + s); System.out.println(map.get(s)); } } } 输出: {String: hi id: 4 hashCode(): 146450=3, String: hi id: 5 hashCode(): 146451=4, String: hi id: 2 hashCode(): 146448=1, String: hi id: 3 hashCode(): 146449=2, String: hi id: 1 hashCode(): 146447=0} Looking up String: hi id: 1 hashCode(): 146447 0 Looking up String: hi id: 2 hashCode(): 146448 1 Looking up String: hi id: 3 hashCode(): 146449 2 Looking up String: hi id: 4 hashCode(): 146450 3 Looking up String: hi id: 5 hashCode(): 146451 4
- 为了速度,散列将键的信息保存在一个数组中,这个信息就是通过键对象的散列函数生成一个散列码,然后将散列码根据某种规则变成数组的下标,只要我们使用键对象就能查找到键在哪个数组的下标中,然后再取出下标中的List,最后根据键从List中取出键值对。
Iterator迭代器
- 迭代器是一个对象,用来遍历并选择集合中的元素。
- 迭代器通常被称为轻量级对象,因为创建它的代价很小。
- Java的迭代器只能单向移动。
- 常用方法:
- 使用容器的iterator()方法返回一个Iterator对象。
- 使用Iterator对象的next()方法获得序列中的下一个元素。
- 使用Iterator对象的hasNext()方法检查序列中是否还有元素。
- 使用Iterator对象的remove()方法将迭代器当前返回的元素删除。调用remove()前必须先调用next()。
-
List<Integer> list1 = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); Iterator<Integer> iterator = list1.iterator(); while (iterator.hasNext()) { Integer integer = iterator.next(); System.out.println("integer = " + integer); } iterator = list1.iterator(); for (int i = 0;i < 3 && iterator.hasNext();i++) { iterator.next(); iterator.remove(); } System.out.println("删除后:" + list1); 输出: integer = 1 integer = 2 integer = 3 integer = 4 integer = 5 删除后:[4, 5]
- ListIterator:
- 它只能用于各种List的访问。
- Iterator只能单向,但ListIterator可以双向移动。
- 常用方法:
- 使用容器的listIterator()方法返回一个ListIterator对象,或使用listIterator(int)方法创建一个一开始就指向列表索引为x的元素的ListIterator对象。
- hasNext():是否有下一个元素;
- next():获得下一个元素;
- hasPrevious():是否有前一个元素;
- previous():获得前一个元素;
- previousIndex():前一个元素的下标;
- nextIndex():当前元素的下一个元素的下标,与next()获得的元素不是同一个元素;
- remove():将迭代器当前返回的元素删除。调用remove()前必须先调用next()或previous();
- set():修改迭代器当前返回的元素,调用set()前必须先调用next()或previous()。
- add():在当前指向出插入一个元素。不必关心next()或previous()。
-
List<Integer> list1 = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); ListIterator<Integer> integerListIterator = list1.listIterator(); while (integerListIterator.hasNext()) { int i = integerListIterator.previousIndex(); boolean has = integerListIterator.hasPrevious(); Integer val1 = null; if (has) { val1 = integerListIterator.previous(); integerListIterator.next(); } Integer val2 = integerListIterator.next(); int j = 0; if (integerListIterator.hasNext()) { j = integerListIterator.nextIndex(); } System.out.println("是否有前一个元素" + has + ", 前一个元素的索引:" + i + ", 元素为:" + val1 + ", 当前元素为:" + val2 + ", 后一个元素的索引:" + j); } integerListIterator = list1.listIterator(3); while (integerListIterator.hasNext()) { integerListIterator.next(); integerListIterator.set(1); } System.out.println(list1); 输出: 是否有前一个元素false, 前一个元素的索引:-1, 元素为:null, 当前元素为:1, 后一个元素的索引:1 是否有前一个元素true, 前一个元素的索引:0, 元素为:1, 当前元素为:2, 后一个元素的索引:2 是否有前一个元素true, 前一个元素的索引:1, 元素为:2, 当前元素为:3, 后一个元素的索引:3 是否有前一个元素true, 前一个元素的索引:2, 元素为:3, 当前元素为:4, 后一个元素的索引:4 是否有前一个元素true, 前一个元素的索引:3, 元素为:4, 当前元素为:5, 后一个元素的索引:0 [1, 2, 3, 1, 1]
-
集合的选择
- List:
- 如果需要执行大量随机访问,使用ArrayList;如果需要执行大量增删操作,使用LinkedList;如果需要支持并发访问,则使用CopyOnWriteArrayList。
- Set:
- 一般选择HashSet,如果需要保证插入的顺序,可以选择 LinkedHashSet;如果需要实现自定义的元素比较方式,可以选择TreeSet;如果需要支持并发访问,可以选择ConcurrentHashMap或CopyOnWriteArraySet。
- Map:
- 如果数据量大,建议使用HashMap;如果需要保证按照键值对的插入顺序来遍历Map,可以使用LinkedHashMap;如果需要在多线程环境中使用,可以考虑使用ConcurrentHashMap。
- 另外:
- 老的集合如Vector(等同ArrayList)、Hashtable(等同HashMap)、Stack(等同Linked用作栈)以及老的集合迭代器Enumeration(等同Iterator)在现在的项目中尽量不要再去使用了,它们已经过时了。
集合的快速报错机制
- Java集合框架中提供了一种快速报错机制,叫作"快速失败"(fail-fast)。这种机制可以在集合被并发修改时,立即抛出异常,而不是等到修改完成之后再发现错误。这样可以更早地发现问题,并提供更清晰地错误信息。
- 快速失败的实现是通过迭代器来实现的。在Java集合中,如果在迭代集合的过程中,发现集合被并发修改了(如添加、删除等),就会立即抛出ConcurrentModificationException异常。这种机制可以确保多线程并发操作集合时,不会导致数据不一致。
- 需要注意的是,快速失败机制只是一种提示机制,不能完全保证线程安全。对于需要多线程并发访问的集合,应该考虑使用线程安全的集合类(如ConcurrentHashMap、CopyOnWriteArrayList等),或者使用同步机制对集合进行保护。