14、集合:
主要包括:
- 集合框架体系;
- Collection:
- List:
- ArrayList;
- LinkedList;
- Vector;
- Set:
- HashSet;
- LinkedHashSet;
- TreeSet。
- List:
- Map:
- HashMap;
- HashTable;
- LinkedHashMap;
- TreeMap;
- Properties。
- Collections。
使用不难,难点在于原理,源码,什么时候使用(应用场景)。
集合的理解和好处:
前面我们保存多个数据使用的是数组,但是数组也有许多不足的地方:
- 长度开始时必须指定,而且一旦指定,不能更改;
- 保存的必须为同一类型的元素;
- 使用数组进行增加元素的示意代码——比较麻烦(创建一个新的数组,长度为原数组的长度+1,将原数组拷贝过来,在末尾加入新数组)。
集合:
- 可以动态保存任意多个对象,使用比较方便;
- 提供了一系列方便的操作对象的方法:add、remove、set、get等;
- 使用集合添加,删除新元素的示意代码——简洁明了。
集合的框架体系:
主要分为两大类:
- 集合主要是两组(单列集合,双列集合)
Collection(单列集合):
在集合里面放的是单个对象。
有==两个重要 子接口:==List 和 Set,他们实现子类都是单列集合。
Map(双列集合):
在集合中放置的是键值对。
Map 接口实现子类 是双列集合,存放的 K-V。
Collection接口和常用方法:
Collection 接口实现类的特点:
public interface Collection<E> extends Iterable<E>
- Collection 实现子类可以存放多个元素,每个元素可以是 Object;
- 有些 Collection 的实现类,可以存放重复的元素,有些不可以;
- 有些 Collection 的实现类,有些是有序的(List),有些是无序的(Set);
- Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的。
Collection 接口常用方法,以实现子类 ArrayList 来演示:
- add:添加单个元素;
- remove:删除指定元素;
- contains:查找元素是否存在;
- size:获取元素个数;
- isEmpty:判断是否为空;
- clear:清空;
- addAll:添加多个元素;
- containsAll:查找多个元素是否都存在;
- removeAll:删除多个元素。
package com.jiangxian.collection_;
import java.util.ArrayList;
import java.util.List;
/**
* @author JiangXian~
* @version 1.0
*/
public class CollectionMethod {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
// add:添加单个元素
list.add("jack");
list.add(10); // 相当于List.add(Integer(10))
list.add(true);
System.out.println("list=" + list);
// remove:删除指定元素
// list.remove(0); // 删除第一个元素
list.remove(true); // 删除特定的某个元素
System.out.println("list=" + list);
// contains:查找元素是否存在
System.out.println(list.contains("jack"));
System.out.println(list.contains(true));
// size:获取元素个数
System.out.println(list.size());
// isEmpty:判断是否为空
System.out.println(list.isEmpty());
// clear:清空
list.clear();
System.out.println("list是否为空:" + list.isEmpty());
// addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("红楼梦");
list2.add("三国演义");
list.addAll(list2);
System.out.println("newlist=" + list);
// containsAll:查找多个元素是否都存在
System.out.println(list.containsAll(list2));
// removeAll:删除多个元素
list.removeAll(list2);
System.out.println("list=" + list);
}
}
Collection 接口遍历元素方式1-使用Iterator(迭代器):
基本介绍:
接口 Iterator——是Collection的父接口
- Iterator 对象称为迭代器,主要是用于遍历 Collection 集合中的元素;
- 所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器;
- Iterator 的结构——看一张图;
- Iterator 仅用于遍历集合,Iterator 本身并不存放对象。
Iterator接口方法:
- hasNext():若迭代器中还有元素,返回true。(即判断是否还有下一个元素)
- next():
- 指针下移;
- 将下移以后集合位置上的元素返回;
- 一开始的指向并不再迭代器内,而是在迭代器外,即一开始为空指向。
- remove():从底层集合中删除此迭代器返回的最后一个元素(可选操作)。
注意:
在调用.next()方法之前必须调用.hasNext()进行检查,若不调用,且下一条记录无效,直接调用时,会抛出异常。
package com.jiangxian.collection_;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
* @author JiangXian~
* @version 1.0
*/
public class CollectionIterator_ {
public static void main(String[] args) {
Collection col = new ArrayList();
col.add(new Book("三国演义","罗贯中",10.1));
col.add(new Book("小李飞刀","古龙",5.1));
col.add(new Book("红楼梦","曹雪芹",34.6));
// System.out.println("col =" + col);
// 我们希望能够遍历 col 集合:
// 1. 先得到 col 对应的迭代器
Iterator iterator = col.iterator();
// 2. 使用 while 循环 =》快捷键itit
while(iterator.hasNext()){
Object obj = iterator.next(); // 向下转型,编译类型为Object,允许类型为 Book
System.out.println(obj);
}
// 3. 当退出 while 循环后,这时iterator 迭代器,指向最后的元素;
// iterator.next(); //NoSuchElementException
// 4. 若希望再次遍历迭代器,需要重置我们的迭代器
// 相当于我们将指针返回到未处理的状态
iterator = col.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}
class Book{
private String name;
private String author;
private double price;
public Book(String name, String author, double price) {
this.name = name;
this.author = author;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}
Collection 接口遍历元素方式2-使用增强for循环:
非常适合用来遍历容器。
在底层其实仍然是一个迭代器。
可以理解成一个简化版本的迭代器。
快捷方式是输入一个:大写的i——I
package com.jiangxian.collection_;
import java.util.ArrayList;
import java.util.Collection;
/**
* @author JiangXian~
* @version 1.0
*/
public class CollectionFor_ {
public static void main(String[] args) {
Collection col = new ArrayList();
col.add(new Book("三国演义","罗贯中",10.1));
col.add(new Book("小李飞刀","古龙",5.1));
col.add(new Book("红楼梦","曹雪芹",34.6));
// 语法:for(数据类型 对象:要遍历的容器对象){}
for(Object obj : col){
System.out.println(obj);
}
}
}
练习:
package com.jiangxian.collection_;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
* @author JiangXian~
* @version 1.0
*/
public class CollectionExercise {
public static void main(String[] args) {
Collection col = new ArrayList();
col.add(new Dog("小白", 3));
col.add(new Dog("小黄", 4));
col.add(new Dog("小黑", 5));
Iterator iterator = col.iterator();
while(iterator.hasNext()){
Object obj = iterator.next();
System.out.println(obj);
}
System.out.println("====增强for循环====");
for(Object obj : col){
System.out.println(obj);
}
}
}
class Dog{
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
List 接口(Collection子接口之一) 和 常用方法:
基本介绍:
List 接口是 Collection 接口的子接口:
- List 集合类中元素有序(即添加顺序和取出顺序一致),且可重复;
- List 集合中的每个元素都有其对应的顺序所以,即支持索引(底层是一个数组);
- List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素;
- JDK API 中 List 接口的常用实现类:
- ArrayList;数组
- LinkedList;链表
- Vector。向量
package com.jiangxian.list_;
import java.util.ArrayList;
import java.util.List;
/**
* @author JiangXian~
* @version 1.0
*/
public class List_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 1.List 集合类中元素有序(即添加顺序和取出顺序一致),且可重复
List list = new ArrayList();
list.add("jack");
list.add("tom");
list.add("marry");
list.add("hsp");
list.add("tom");
System.out.println(list);
// 2. List 集合中的每个元素都有其独赢的顺序索引,支持索引(从0开始)
System.out.println(list.get(0));
}
}
常用方法:
List 中添加了一些根据索引来操作集合元素的方法:
- void add(int index, Object ele):在 index 位置插入 ele 元素;
- boolean addAll(int index, Collection eles):从 index 位置开始将eles中的所有元素添加进来;
- Object get(int index):获取指定 index 位置的元素;
- int indexOf(Object obj):返回 List 中第一次出现 obj 的 index;
- int lastIndexOf(Object obj):返回 List 中最后一次出现 obj 的 index;
- Object remove(int index):删除指定位置的元素,并返回该元素;
- Object set(int index, Object ele):设置指定位置的元素为 ele ,相当于是替换;
- List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex-1 位置的子集合(左闭右开)。
package com.jiangxian.list_;
import java.util.ArrayList;
import java.util.List;
/**
* @author JiangXian~
* @version 1.0
*/
public class ListMethod_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
list.add("jack");
list.add("tom");
list.add("marry");
list.add("hsp");
list.add("tom");
System.out.println(list);
// 1. void add(int index, Object ele):在 index 位置插入 ele 元素;
System.out.println("==================void add==========");
list.add(0,"刘备");
System.out.println(list);
// 2. boolean addAll(int index, Collection eles):从 index 位置开始将eles中的所有元素添加进来;
System.out.println("=========boolean addAll==========");
List list1 = new ArrayList();
list1.add("刘邦");
list1.add("项羽");
list.addAll(list1);
System.out.println(list);
// 3. Object get(int index):获取指定 index 位置的元素;
System.out.println("=========Object get==========");
System.out.println(list.get(0));
// 4. int indexOf(Object obj):返回 List 中第一次出现 obj 的 index;
System.out.println("================int indexOf=================");
System.out.println(list.indexOf("刘邦"));
// 5. int lastIndexOf(Object obj):返回 List 中最后一次出现 obj 的 index;
System.out.println("=========int lastIndexOf==========");
System.out.println(list.lastIndexOf("tom"));
// 6. Object remove(int index):删除指定位置的元素,并返回该元素;
System.out.println("=========Object remove==========");
System.out.println(list.remove(0));
// 7. Object set(int index, Object ele):设置指定位置的元素为 ele ,相当于是替换;
System.out.println("=========Object set==========");
System.out.println(list.set(0, "曹操")); // 返回的是将被修改的原始元素
System.out.println(list.get(0));
// 8. List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex-1 位置的子集合(左闭右开)。
System.out.println("=========List subList==========");
System.out.println(list.subList(0, 2));
}
}
三种遍历方式:
- 使用迭代器 iterator;
- 使用增强 for 循环;
- 使用普通 for 循环。
ArrayList(List的实现类之一):
ArrayList 的注意事项:
- permits all elements, including null,即 ArrayList 可以存放所有类型的元素,也包括 null(会被转换为String 类型的"null"),并且可以多个;
- ArrayList 底层是由数组来实现存储的;
- ArrayList 基本等同于 Vector,除了 ArrayList 是线程不安全(执行效率高),多线程的情况下,不建议使用 ArrayList。
ArrayList 底层结构和源码分析(重点):
- ArrayList 中维护了一个 Object 类型的数组 elementData。
- transient Object[] elementData;
- transient 关键字——表示瞬间,短暂的;表示该属性不会被序列化。
- 当创建 ArrayList 对象时,若使用无参构造器,则初始 elementData 容量为 0,第1次添加,则扩容elementData 为10,若需要再次扩容,则扩容 elementData 为1.5倍;
- 若使用的是指定大小的构造器,则初始 elementData 容量为指定大小,若需要扩容,则直接扩容 elementData 为1.5倍。
add 以及 扩容机制源码解读:
package com.jiangxian.list_.arrayList_;
import java.util.ArrayList;
import java.util.Iterator;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class ArrayListSource_ {
public static void main(String[] args) {
// 使用无参构造器创建 ArrayList对象
ArrayList list = new ArrayList();
/*
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
ctrl + 鼠标左键 DEFAULTCAPACITY_EMPTY_ELEMENTDATA:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
我们发现其就是一个空数组
所以我们创建了一个空elementData数组
*/
// ArrayList list = new ArrayList(8);
for (int i = 1; i <= 10; i++) {
list.add(i);
/*
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确定容量够不够
elementData[size++] = e; // 然后再执行赋值操作
return true;
}
*/
/*
private void ensureCapacityInternal(int minCapacity) { // minCapacity 初始值为1
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 判断传进来的是不是空
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
// 取最大值为minCapacity
// private static final int DEFAULT_CAPACITY = 10;
所以最大值为 10,minCapacity 现在的值为10.
}
// 上面的步骤是为了确定一个真正的 minCapacity,下面才是判断是否需要扩容
ensureExplicitCapacity(minCapacity);
}
*/
/*
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // modCount记录集合被修改的次数,一开始为0
// overflow-conscious code
if (minCapacity - elementData.length > 0)
// 若需要的最小容量大于 数组的长度,调用底层的grow
grow(minCapacity);
}
*/
/*
private void grow(int minCapacity) {
// overflow-conscious code
// 将原来数组的长度,存放在OldCapacity中
int oldCapacity = elementData.length;
// 新容量 = 原始长度加上原始长度/2,即原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若计算出来的新容量比 minCapacity 小,就仍然使用 minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 若新的容量比 MAX_ARRAY_SIZE 大,使用 hugeCapacity方法
// private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
*/
}
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(100);
list.add(200);
for (Object o : list) {
System.out.println(o);
}
}
}
有参构造器就是在构造器的源码有差别,其它没有差别。
Vector(List的实现类之一):
- Vector 底层也是一个对象数组,protected Object[] elementData;
- Vector 是线程同步的,即线程安全,Vector 类的操作方法带有 sychronized;
- 在开发中,需要线程同步安全时,考虑使用 Vector。
Vector 和 ArrayList 的比较:
底层结构 | 版本 | 线程安全(同步)效率 | 扩容倍数 | |
---|---|---|---|---|
ArrayList | 可变数组 | jdk1.2 | 不安全,效率高 | 若时有参构造——1.5倍;若是无参——1.第一次设置为10;2.第二次扩容为1.5倍 |
Vector | 可变数组 | jdk1.0 | 安全,效率不高 | 若是无参,默认10,填满后,按两倍扩容;若指定大小,则每次直接按两倍扩容。 |
底层结构与源码分析:
package com.jiangxian.list_.vector_;
import java.util.Vector;
/**
* @author JiangXian~
* @version 1.0
*/
public class Vector_ {
public static void main(String[] args) {
Vector vector = new Vector();
/*
public Vector() {
this(10); // 使用无参构造器,容量会被设置为10
}
*/
/*
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
*/
/*
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
*/
for(int i = 0; i < 10; i++){
vector.add(i);
/*
public synchronized boolean add(E e) {
modCount++; // 修改次数,开始为0,在AbstractList中定义
ensureCapacityHelper(elementCount + 1); // 确定现在的容量是否足够
elementData[elementCount++] = e;
return true;
}
*/
/*
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
*/
/*
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// capacityIncrement = 0
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
*/
}
}
}
LinkedList(List的实现类之一):
- LinkedList 底层实现了双向链表和双端队列特点;
- 可以添加任意元素(元素可以重复),包括null;
- 线程不安全,没有实现同步。
LinkedList 的底层操作机制:
- LinkedList 底层维护了一个双向链表;
- LinkedList 中维护了两个属性 first 和 last 分别指向 首节点 和 尾节点;
- 每个节点(Node对象),里面又维护了 prev、next、item 三个属性,其中通过 prev 指向前一个,通过 next 指向下一个。最终实现双向队列。
- 所以 LinkedList 的元素的添加和删除,不是通过数组完成的,相对来说效率较高。
Node 演示:
package com.jiangxian.list_.LinkedList_;
/**
* @author JiangXian~
* @version 1.0
*/
public class LinkedList01 {
public static void main(String[] args) {
// 模拟一个简单的双向链表
Node jack = new Node("jack");
Node queen = new Node("queen");
Node tom = new Node("tom");
// 连接三个节点,形成双向链表
// jack -> queen -> tom
jack.next = queen;
queen.next = tom;
// tom -> queen -> jack
queen.pre = jack;
tom.pre = queen;
Node first = jack; // 让 first 引用指向 jack,就是双向链表的首节点
Node last = tom; // 让 last 引用指向 tom,就是双向链表的尾节点
// 演示,从头到尾的遍历
System.out.println("=====从头到尾遍历=====");
while(true){
if(first == null){
break;
}
// 输出 first 信息
System.out.println(first);
first = first.next;
}
// 演示,从尾到头
System.out.println("=====从尾到头遍历=====");
while(true){
if(last == null){
break;
}
System.out.println(last);
last = last.pre;
}
// 演示链表的添加对象/数据,是多么的方便
// 1. 先创建一个要添加的 Node
Node smith = new Node("smith");
queen.next = smith;
smith.pre = queen;
smith.next = tom;
tom.pre = smith;
first = jack;
System.out.println("=====从头到尾遍历=====");
while(true){
if(first == null){
break;
}
// 输出 first 信息
System.out.println(first);
first = first.next;
}
last = tom;
System.out.println("=====从尾到头遍历=====");
while(true){
if(last == null){
break;
}
System.out.println(last);
last = last.pre;
}
}
}
// 定义一个Node类,Node对象,表示双向链表的一个点
class Node{
public Object item; // 真正存放数据
public Node next; // 指向下一个节点
public Node pre; // 指向前一个节点
public Node(Object name){
this.item = name;
}
@Override
public String toString() {
return item.toString();
}
}
LinkedList的增删改查:
package com.jiangxian.list_.LinkedList_;
import java.util.LinkedList;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class LinkedListCRUD {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.add(1);
/*
public boolean add(E e) {
linkLast(e);
return true;
}
*/
/*
将传入的元素添加进双向列表:
void linkLast(E e) {
final Node<E> l = last; // 将原来的last 传递给final修饰的 l
final Node<E> newNode = new Node<>(l, e, null);
// 创建一个新的节点,前节点是 last(尾插法),值是传入的e,尾节点是 null
last = newNode; // 插入在末尾的元素自然是最后一个元素
if (l == null) // 若以前的双向链表中没有元素,即开始的last为空
first = newNode; // 那么现在里面只有一个元素,所以头节点和尾节点,都指向新添加的节点
else
l.next = newNode; // 否则,将l即原始的last的下一个指向修改为新添加的节点
size++; // 链表中元素的个数+1
modCount++; // 修改次数加一
}
*/
linkedList.add(2);
/*
public boolean add(E e) {
linkLast(e);
return true;
}
*/
/*
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
*/
linkedList.add(3);
System.out.println("linkedList=" + linkedList);
// 演示删除最后的节点(注意有多种删除,仅演示一种删除first指向的节点)
linkedList.remove();
System.out.println("linkedList=" + linkedList);
/*
public E remove() {
return removeFirst();
}
*/
/*
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f); // 真正实现删除首节点的代码
}
*/
/*
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item; // 将首节点的值提取出来,作为返回值
final Node<E> next = f.next; // 将首节点指向的下一个节点提取出来
f.item = null; // 将原始首节点的值设置为空
f.next = null; // help GC,将原始首节点的下一个指向设置为空
first = next; // 将首节点设置为next
if (next == null)
last = null;
else
next.prev = null; // 将现在首节点的前指针指向设置为空,不然会指向原来的first
size--; // 双向链表的长度减一
modCount++; // 修改次数加一
return element; // 返回原来节点中的值
}
*/
// 修改:
linkedList.set(1, "abc");
/*
public E set(int index, E element) {
checkElementIndex(index); // 检验是否是合法的index
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
*/
/*
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
*/
/*
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
*/
/*
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { // 当下标比链表长度的一半小时
Node<E> x = first; // 设置x为 首节点
for (int i = 0; i < index; i++) // 遍历要查询的 index 次
x = x.next; // 一直更新 x 为 后一个节点
return x; // 直到x的下标为index位置,返回(链表不是顺序存储所以查找麻烦)
} else {
// 否则,反向遍历即可
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
*/
System.out.println("linkedList=" + linkedList);
// 查找
linkedList.get(1);
/*
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// 和上面的修改是一样的,自己领悟下。
*/
}
}
这里的源码涉及到数据结构,没学过数据结构的理解起来可能比较困难。
ArrayList 和 LinkedList 的比较:
底层结构 | 增删的效率 | 改查的效率 | |
---|---|---|---|
ArrayList | 可变数组 | 较低,数组扩容 | 较高 |
LinkedList | 双向链表 | 较高,通过链表节点追加 | 较低 |
- 若改查操作较多,使用ArrayList;
- 若增删操作较多,使用LinkedList;
- 都是线程不安全。
Set 接口(Collection子接口之一)和常用方法:
Set基本介绍:
- 无序(添加和取出元素的顺序不一样,但是取出的顺序是固定的),没有索引;
- 不允许重复元素,所以最多包含一个null;
- JDK API 中 Set 接口的实现类常用的有:
- HashSet;
- TreeSet。
Set接口的常用方法:
和 List 接口一样,Set 接口也是 Collection 的子接口,所以常用方法和 Collection 接口一样。
Set接口的常用遍历方法:
同 Collection 的遍历方式一样,因为 Set 接口是 Collection 接口的子接口:
- 可以使用迭代器iterator;
- 可以使用增强for循环;
- 不能使用索引方式来获取。
package com.jiangxian.set_;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class SetMethod {
public static void main(String[] args) {
// 1. 以Set接口的实现类 HashSet 来讲解 Set 接口的方法;
// 2. Set接口的实现类的对象(Set 接口对象),不能存放重复的元素,可以添加一个 null
// 3. Set接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
// 4. 注意:取出的顺序虽然不是添加的顺序,但是顺序是固定的
Set set = new HashSet();
set.add("jack");
set.add("john");
set.add("jack"); // 重复
set.add("john");
set.add("hsp");
set.add("mary");
set.add(null);
set.add(null);
System.out.println("set=" + set);
// set=[null, hsp, mary, john, jack]
// 遍历:
// 方式1 :iterator
Iterator iterator = set.iterator();
while(iterator.hasNext()){
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
// 方式2 :增强for循环:
System.out.println("========增强for循环=======");
for(Object o : set){
System.out.println("o=" + o);
}
}
}
HashSet(Set的实现类之一):
HashSet的全面说明:
-
HashSet 实现了 Set 接口;
-
HashSet 实际上是 HashMap,源码如下:
public HashSet() { map = new HashMap<>(); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
-
可以存放 null 值,但是只能有一个 null;
-
HashSet 不保证元素是有序的,取决于 hash 后,再确定索引的结果。(即不能保证存放元素的顺序和取出顺序一致);
-
不能有重复元素/对象,再前面 Set 接口使用已经讲过。
底层机制说明:
模拟数组链表:
对结构的了解。
package com.jiangxian.set_;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class HashSetStructure {
public static void main(String[] args) {
// 模拟一个 HashSet 底层——实际上就是 HashMap 底层
// 1. 创建一个数组,数组的类型是 Node[]
// 2. 有些人直接将 Node[] 数组称为表(Table)
Node[] table = new Node[16];
System.out.println("table = " + table);
// 3. 创建节点
Node john = new Node("john", null);
table[2] = john;
Node jack = new Node("jack", null);
john.next = jack; // 将jack节点挂载到john节点下
Node rose = new Node("rose", null);
jack.next = rose;
System.out.println("table = " + table);
}
}
class Node{
public Object item;
public Node next;
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
源码:
分析HashSet 的添加元素底层是如何实现的。
- HashSet 的底层是 HashMap;
- 添加一个元素时,先得到 hash 值,会转换成索引值。
- 找到存储数据表 table,看这个索引位置是否已经存放有元素;
- 若没有,直接加入;
- 若有,调用 equals 进行比较,若相同,放弃添加,若不同,则添加到最后(说的是链表的最后,参照上面的数组lian’bi);
- 再Java8中,若一条链表的元素个数超过 TREEIFY_THRESHOLD(默认时8),并且table 的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进化成红黑树。
第一次添加与第二次添加源码解读:
package com.jiangxian.set_;
import java.util.HashSet;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
// 1. 执行HashSet()
/*
public HashSet() { // 构造函数,底层是HashMap
map = new HashMap<>();
}
*/
hashSet.add("java"); // 到此处,第一次 add 添加完成。
// 2. 执行 add()
/*
public boolean add(E e) { // e就是我们传入的对象,此处为java,E表示泛型。
return map.put(e, PRESENT)==null;
// private static final Object PRESENT = new Object();
}
*/
/*
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
*/
// 使用 force step into 强行进入hash(key),这个是用来计算hash值的
/*
static final int hash(Object key) {
int h;
// 可以看到,hash并不完全是hashCode,而是经过算法优化的结果
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
*/
/*
// 是最重要的,需要仔细体会
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
// 这一行,是我们定义的辅助变量,tab为table的复制,p为要对应索引位置处的空间
// n为tab的容量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table是HashMap的一个属性,放Node节点的数据
//transient Node<K,V>[] table;
// 由源码可值,一开始为空
if ((tab = table) == null || (n = tab.length) == 0)
// 将table赋值给tab,判断是否为空,或者长度是否为0
// 这个代码块较长,放置在紧接的代码块中解释。
n = (tab = resize()).length;
// 将tab[i = (n - 1) & hash]存储的节点赋值给p,判断是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
// 为空,就直接将新节点填入
tab[i] = newNode(hash, key, value, null);
else {
// 一个开发技巧提示:在需要局部变量(辅助变量)时候,在需要的时候创建
Node<K,V> e; K k;
// p实际上是指向我们当前的索引位置对应的链表的第一个元素
// 1.若p的hash与我们想要添加的key的hash值相等
// 2.同时满足 key 和 p的key属性一样或者是key不为空且内容与p的key属性一样
// 所以我们需要看下对象的equals是否被重写
// 就不能加入,跳转到if (e != null),return oldValue;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 那么将 p 赋值给 辅助变量 e
e = p;
// 判断 p 是不是一棵红黑树
// 若是一颗红黑树,就调用 putTreeVal(暂时不展开),来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 只剩下一种情况了,就是 p 为一条链表
// 1.依次去和链表中的各个节点进行比较,若都不相同,遍历到null,将null 赋值为空,
// 添加新节点到末尾,若bigCount达到了TREEIFY_THRESHOLD,转换为红黑树
// 在添加新节点后,立即判断 该链表是否已经达到8个节点,若满足,要对当前链表进行树化
// 但是在进行树化的时候,还需要一个对table 长度的判断,若小于 64 也不会马上树化
// 此时会先进行扩容
// 2.若与链表中的一个节点相同,那么跳出循环
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 为下一次循环做准备,因为下一次循环的p为p.next,e中存储的是p.next
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 修改次数自增
++modCount;
// 判断容量大小,并判断是否超过门槛
if (++size > threshold)
resize();
// 这个方法现在是空,在LinkedHashSet中实现
afterNodeInsertion(evict);
// 添加成功就返回空
return null;
}
*/
hashSet.add("hsp");
}
}
对resize()的源码解读
final Node<K,V>[] resize() {
// oldTab 用于存放原始的 table
Node<K,V>[] oldTab = table;
// oldCap 用于存放原始的通量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr 用于存放阈值,用于防止多线程出现拥堵
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 由于是第一次,所以 oldCap = 0,执行这段代码
else { // zero initial threshold signifies using defaults
// static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 为16
newCap = DEFAULT_INITIAL_CAPACITY;
// static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认的缓冲系数
// 现在 newThr 为12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将新的 newTab 存放在属性table中
table = newTab;
// 由于第一次添加,oldTab = null,所以这一段代码块不执行
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新的newTab
return newTab;
}
扩容和转换成红黑树:
- HashSet 底层是 HashMap,第一次添加时,table 数组从0扩容到了 16,临界值(threshold)是16*加载因子(loadFactor)——0.75 = 12;
- 若 table 数组使用到了临界值 12,就会执行扩容,16 * 2 = 32,那么新的临界值就是 32 * 0.75 = 24
- 在Java8中,若一条链表的元素个数达到了 (TREEIFY_THRESHOLD)默认为8,并且 table的大小 >= MIN_TREEIFY_CAPACITY(默认为64),就会进行树化为红黑树,否则仍然采用数组。
扩容:
扩容发生在哪里呢?当我们每次加入一个节点,不管节点是加在 table表的空位置,还是在链表上,都会使得++size。
package com.jiangxian.set_;
import java.util.HashSet;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class HashSetIncrement {
public static void main(String[] args) {
/*
1. HashSet 底层是 HashMap,第一次添加时,table 数组从0扩容到了 16,
临界值(threshold)是16*加载因子(loadFactor)——0.75 = 12;
2. 若 table 数组使用到了临界值 12,就会执行扩容,16 * 2 = 32,
那么新的临界值就是 32 * 0.75 = 24
*/
HashSet hashSet = new HashSet();
for (int i = 0; i < 100; i++) {
hashSet.add(i);
}
}
}
final Node<K,V>[] resize() {
// oldTab 用于存放原始的 table
Node<K,V>[] oldTab = table;
// oldCap 用于存放原始的通量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr 用于存放阈值,用于防止多线程出现拥堵
int oldThr = threshold;
int newCap, newThr = 0;
// oldCap>0说明非第一次执行resize
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) { // 一般是不会超过最大容量的,所以我们看下一个else-if
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 若两倍 oldCap 比最大值小,且oldCap 大于等于默认值16
// 那么新的容量为 oldCap的两倍,代码块中执行 newThr = 2倍的oldThr
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 由于是第一次,所以 oldCap = 0,执行这段代码
else { // zero initial threshold signifies using defaults
// static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 为16
newCap = DEFAULT_INITIAL_CAPACITY;
// static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认的缓冲系数
// 现在 newThr 为12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将新的 newTab 存放在属性table中
table = newTab;
// 由于第一次添加,oldTab = null,所以这一段代码块不执行
if (oldTab != null) { // 这段代码暂时我也没看明白
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新的newTab
return newTab;
}
转换成红黑树:
package com.jiangxian.set_;
import java.util.HashSet;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class HashSetIncrement {
public static void main(String[] args) {
/*
1. HashSet 底层是 HashMap,第一次添加时,table 数组从0扩容到了 16,
临界值(threshold)是16*加载因子(loadFactor)——0.75 = 12;
2. 若 table 数组使用到了临界值 12,就会执行扩容,16 * 2 = 32,
那么新的临界值就是 32 * 0.75 = 24
*/
HashSet hashSet = new HashSet();
// for (int i = 0; i < 100; i++) {
// hashSet.add(i);
// }
/*
在Java8中,若一条链表的元素个数达到了 (TREEIFY_THRESHOLD)默认为8,
并且 table的大小 >= MIN_TREEIFY_CAPACITY(默认为64),
就会进行树化为红黑树,否则仍然采用数组扩容机制,在扩容后需要重写计算hash。
*/
for (int i = 1; i <= 12; i++) {
hashSet.add(new A(i));
// 什么意思呢?
// 在执行到第9次时,会执行一次resize,从16扩容到32
// 执行到第10次时,会再执行一次resize,从32扩容到64
// 执行到11次时,会进行树化(红黑树,在数据结构中再深入)
}
}
}
class A{
private int n;
public A(int n) {
this.n = n;
}
// 为了确保hashCode一样
@Override
public int hashCode() {
return 100;
}
}
第一次扩容:索引在4,节点类型为Node,还是链表,此时容量为16:
第二次扩容:索引在4,节点类型为Node,还是链表,此时容量为32:
第三次扩容:索引在36,节点类型为Node,还是链表,此时容量为64:
然后便不再执行扩容,而是将该链表转换为红黑树,节点类型为TreeNode,table的容量仍然时64:
关于红黑树,此处不展开,另外扩容后的hash更改,应该在resize处,但是resize第一次后的源码暂时我也没看懂,所以不写,以免误导。
Exercise:
Exercise1:
定义一个Employee 类,该类包含:private成员属性——name,age;
- 创建3个Employee 对象放入 HashSet;
- 当 name 和 age 的值相同时,认为是相同员工,不能添加到 HashSet 集合中。
package com.jiangxian.set_;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class HashSetExercise01 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("milan",18,10000));
hashSet.add(new Employee("smith",18,10000));
hashSet.add(new Employee("milan",18,20000));
System.out.println(hashSet);
}
}
class Employee{
private String name;
private int age;
private double salary;
public Employee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
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 double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
@Override
public boolean equals(Object o) {
// 判断是否相等,首先,判断是否是同一个对象
if (this == o) return true;
// 若传入的数据为空,或者类型不一致返回false
if (o == null || getClass() != o.getClass()) return false;
// 向下转型
Employee employee = (Employee) o;
return age == employee.age && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
// 下面为计算hash的源码
// public static int hash(Object... values) { 传入的是一个可变参数
// return Arrays.hashCode(values);
// }
// public static int hashCode(Object a[]) { 计算的是一个数组的hash
// if (a == null)
// return 0;
//
// int result = 1;
//
// for (Object element : a) // 使用增强for循环,将内部的值遍历,并计算到result中
// result = 31 * result + (element == null ? 0 : element.hashCode());
//
// return result;
// }
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
", salary=" + salary +
'}';
}
}
Exercise02:
定义一个Employee类,该类包含:private成员属性——name,sal,birthday(MyData类型——包含属性:year,month,day)
package com.jiangxian.set_;
import java.util.HashSet;
import java.util.Objects;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class HashSetExercise02 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee_("Smith",21,new MyData("12","11","1")));
hashSet.add(new Employee_("Smith",21,new MyData("12","11","1")));
hashSet.add(new Employee_("Smith",21,new MyData("12","11","1")));
System.out.println(hashSet);
}
}
class Employee_{
private String name;
private int age;
MyData birthday;
public Employee_(String name, int age, MyData birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public MyData getBirthday() {
return birthday;
}
public void setBirthday(MyData birthday) {
this.birthday = birthday;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee_ employee = (Employee_) o;
return age == employee.age && Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, age, birthday);
}
@Override
public String toString() {
return "Employee_{" +
"name='" + name + '\'' +
", age=" + age +
", birthday=" + birthday +
'}';
}
}
class MyData{
private String year;
private String month;
private String day;
public MyData(String year, String month, String day) {
this.year = year;
this.month = month;
this.day = day;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public String getMonth() {
return month;
}
public void setMonth(String month) {
this.month = month;
}
public String getDay() {
return day;
}
public void setDay(String day) {
this.day = day;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyData myData = (MyData) o;
return Objects.equals(year, myData.year) && Objects.equals(month, myData.month) && Objects.equals(day, myData.day);
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
//[Employee_{name='Smith', age=21, birthday=com.jiangxian.set_.MyData@4554617c}, Employee_{name='Smith', age=21, birthday=com.jiangxian.set_.MyData@74a14482}, Employee_{name='Smith', age=21, birthday=com.jiangxian.set_.MyData@1540e19d}]
// 这是不重写MyData的hashCode方法的结果,我们发现由于birthday的hashCode不同,即使重写了equals也没有意义
@Override
public String toString() {
return "MyData{" +
"year='" + year + '\'' +
", month='" + month + '\'' +
", day='" + day + '\'' +
'}';
}
}
若不重写MyData的hashCode(),会导致计算Employee的hash值都是不同的,这是为什么呢?
其实我们上面贴出过源码能很好的解释这一点:
此时我们传进a[]的参数有:name,age 和 MyData;
name是String,age是int类型的hashCode已经被重写过了,所以特定的值返回的hashCode都是一样的;
但是由于我们MyData的hashCode()没有被重写,所以,其每个对象的hashCode的计算按默认来计算,导致不同对象的hashCode一定不同;
所以我们在add时,由于hash值都不同,就直接添加了。
// public static int hashCode(Object a[]) { 计算的是一个数组的hash
// if (a == null)
// return 0;
//
// int result = 1;
//
// for (Object element : a) // 使用增强for循环,将内部的值遍历,并计算到result中
// result = 31 * result + (element == null ? 0 : element.hashCode());
//
// return result;
// }
LinkedHashSet(Set的实现类之一):
LinkedHashSet的全面说明:
-
LinkedHashSet 是 HashSet 的子类;
-
LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组 + 链表 + 双链表(怎么理解呢?就是在HashMap的Node基础上,又多了两个指针,用来指向按顺序添加的节点);
-
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置(和HashSet一样),同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的,实际不然,只是逻辑有序,不是物理有序,物理上的存储和HashSet一摸一样,逻辑的有序是通过多的双链表实现的;
-
LinkedHashSet 不允许重复元素。
-
table 为 Node[]的类型,但是内部存放的元素为 Entry(LinkedHashSet的,与后文map中的Entry不是一样的!!!!那个里面的Entry是接口),其为Node的子类,添加了before 和 after 两个指针,用来实现双向链表。
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
package com.jiangxian.set_;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class LinkedHashSetSource {
public static void main(String[] args) {
Set set = new LinkedHashSet();
/*
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next); // 这个调用的就是 HashMap.Node(hash, key, value, next)
}
}
*/
/*
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
// 将新添加的节点置为尾节点tail,然后将双向链表形成
// 将tail赋值给last
LinkedHashMap.Entry<K,V> last = tail;
// 将传入的节点设置为尾节点 tail
tail = p;
if (last == null)
head = p;
else { // 形成双向链表
p.before = last;
last.after = p;
}
}
*/
set.add(new String("AA"));
set.add(456);
set.add(456);
set.add(123);
System.out.println("set=" + set);
}
}
除此以外,大部分的操作都和HashSet一摸一样,不再赘述。
TreeSet(Set的实现类之一):
其可以实现排序,因为内置有一个Comparator 类型的属性。在调用无参构造器时,直接使用的是compareTo,在调用有参构造器时,我们可以重写Comparator中的 compare方法,实现按我们想要的方式排序:
package com.jiangxian.set_;
import java.util.Comparator;
import java.util.TreeSet;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class TreeSet_ {
public static void main(String[] args) {
// 1.当我们使用无参构造器,创建TreeSet时,仍然是无序的
// 2.使用TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
// 并可以指定排序规则
// 浅浅看看个源码:
/*
1. 构造器将传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap属性 comparator
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2. 执行add操作:
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
private transient Entry<K,V> root;
public V put(K key, V value) {
Entry<K,V> t = root; // 创建一个t,将root赋值给他,一开始root为null
if (t == null) {// 当t为空时,即第一次
compare(key, key); // type (and possibly null) check
// 这个比较只是为了防止key为空,且没有返回值
root = new Entry<>(key, value, null);
// 将新的Entry存入root(),且此处的Entry不是map.Entry这个接口了,而是一个内部类(与Node类似)
size = 1; // 将内部实际存储的数据个数记录为size
modCount++; // 修改次数自增
return null; // 返回null
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
// cpr是我们传入的 comparator对象,若没有,就是空
Comparator<? super K> cpr = comparator;
if (cpr != null) { // 当其不为空,即我们使用的是有参构造器
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else // 若相等,则添加不进去了,更改value(在set中没有意义)
return t.setValue(value);
} while (t != null);
}
else { // 若使用的是无参构造器,使用compareTo方法
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
*/
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo((String)o2);
}
});
// 添加元素:
treeSet.add("jack");
treeSet.add("tom");
treeSet.add("smith");
treeSet.add("a");
System.out.println(treeSet);
}
}
Map 接口和常用方法:
Map 接口实现类的特点(很实用):
存放双列k-v;
Set也用k-v,k也是存放我们传入的对象,但是v的值确是确定的,是Present(Object,仅仅用来占位)。
此处仅讲JDK8的Map接口的特点:
- Map 和 Collection 是并列存在的,用于保存具有映射关系的数据:Key-Value(键值对);
- Map 中的 key 和 value 可以是任何引用类型的数据,其会被封装到 HashMap$Node 对象中,也就是说Map中的键值对(Key-Value)实际上就是存放在 Node 类型中的;
- Map 中的 key 不允许重复,所以只能最多有一个空;
- Map 中的 value 可以重复,所以可以有多个空;
- 我们常用 String 类作为 Map 的 key;
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value。
- Map 存放数据的key-value示意图,一对k-v 是放在 HashMap$Node 中的,又因为 Node 实现了 Entry 接口,有些书上也说一对k-v就是一个Map.Entry。
源码:
Example:
package com.jiangxian.map_.mapsource_;
import java.util.HashMap;
import java.util.Map;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class MapSource {
public static void main(String[] args) {
// 1. Map 与 Collection 并列存在,用于保存具有映射关系的数据;
// 2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
// 3. Map 中的 key 不允许重复,所以只能有一个 null;
// 4. Map 中的 value 可以冲覅,所以可以有多个 null;
// 5. 经常使用 String 类作为 Map 的 key
// 6. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到指定的 key 总能找到对应的 value
Map map = new HashMap();
map.put("no1","韩顺平"); // 源码在HashSet中看过了。
map.put("no2","张无忌");
map.put("no1","张三丰"); // 当有相同的k,就等价于替换,用新的value去替换oldValue。
System.out.println(map.get("no1"));
System.out.println("map=" + map);
}
}
putVal:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 此处会和HashSet的结果不同,为什么呢?我们来分析下:
// 当e不为空时,说明key有相同的,即有重复的元素
// HashSet 中的 value 都是一样的,key 也都是一样的,所以没有什么影响,就是不添加
// 但是Map中的value是有实际意义的,不再是HashSet中起到占位作用的了
// 它会将新的value覆盖原本的value。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get(key):
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
Key-Value的存储:
一对k-v就是放在 HashMap$Node 中的,我们怎么得到这个结论的呢,从putval的源码中有这样一段代码:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
newNode的返回类型是啥呢?
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
可以看到我们存放到tab中的是一个Node类型的节点,那么自然,一对k-v是存放在Node中的啦,那么Node这个内部类是什么样的呢?我们可以看到,Node中有一个接口,Map.Entry。怎么去理解下面的这种图呢,我们会把指向实际存放k-v的Node引用存放进一个Map.Entry类型的Entry中,Entry组成的Set叫做EntrySet,在
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
// 1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null);
// 2. k-v 为了方便程序员遍历,还会创建 EntrySet(是一个集合)
// 该集合存放的元素类型是Map.Entry,而一个Entry对象就包含key和value
// 源码——transient Set<Map.Entry<K,V>> entrySet;
// 在这段代码中,我们只是让K指向了Node中的key,V指向了Node中的value,
// 只是一个简单的指向,没有存放新的东西,
// 所以当我们迭代EntrySet时,实际上是在迭代HashMap中所有的Node对象
// 3. entrySet 中,定义的类型是 Map.Entry,但实际存放的是 HashMap$Node 的类型,为什么呢,
// 这是因为Node实现了Map.Entry 这个接口,所以运行类型是 Node(编译类型是Map.Entry,运行类型是Node)
// 4. 这样,当把 HashMap$Node 对象的引用存放到 entrySet 就方便我们进行遍历,其提供了两个非常重要的方法
// getKey和getValue
欸,那我们为什么要这么写呢,Node明明实现类Entry接口,那么肯定有getKey 和 getValue方法呀?
看下源码,我们发现了什么?Node类型是一个默认类型的内部类,所以我们在外部是不能够调用的!
static class HashMap. Node<K, V> implements Map. Entry<K, V>
而Map.Entry呢?
public static interface Map. Entry<K, V>
我们来看一下代码:
package com.jiangxian.map_.mapsource_;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class MapSource {
public static void main(String[] args) {
// 1. Map 与 Collection 并列存在,用于保存具有映射关系的数据;
// 2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
// 3. Map 中的 key 不允许重复,所以只能有一个 null;
// 4. Map 中的 value 可以冲覅,所以可以有多个 null;
// 5. 经常使用 String 类作为 Map 的 key
// 6. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到指定的 key 总能找到对应的 value
Map map = new HashMap();
map.put("no1","韩顺平"); // 源码在HashSet中看过了。
map.put("no2","张无忌");
map.put("no1","张三丰"); // 当有相同的k,就等价于替换,用新的value去替换oldValue。
System.out.println(map.get("no1"));
System.out.println("map=" + map);
Set set = map.entrySet();
for (Object object : set) {
Map.Entry entry = (Map.Entry) object;
System.out.println(entry.getKey() + "-" + entry.getValue());
}
// keySet就是我们图上的keySet
Set set1 = map.keySet();
for (Object object : set1) {
System.out.println(object );
}
// values就是我们图上的values
Collection collection = map.values();
for (Object object : collection) {
System.out.println(object );
}
}
}
我们发现了一个小问题,为什么一个接口可以有实例化的对象呢?
这其实是一个理解误区,这不是实例化,只是类型转换(向下转换)
Map.Entry entry = (Map.Entry) object;
所以这段代码仅仅是类型转换,而不是实例化,实例化需要new,是一个创建对象的过程。
Map常用方法:
- map.put(Object key,Object value):添加k-v;
- map.remove(Object key):根据键删除映射关系;
- map.get(Object key):根据键返回值;
- map.size():获取元素的个数;
- map.clear():清除k-v;
- map.containsKey():查找键是否存在;
package com.jiangxian.map_.mapmethod_;
import java.util.HashMap;
import java.util.Map;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class MapMethod {
public static void main(String[] args) {
// 演示Map的常用方法:
System.out.println("========put==========");
Map map = new HashMap();
map.put("邓超",new Book("",100));
map.put("邓超","孙俪");
map.put("JiangXian",null);
map.put(null,"JiangXian");
System.out.println("map=" + map);
// remove
System.out.println("========remove========");
map.remove(null);
System.out.println("map=" + map);
// get
System.out.println("=======get==========");
System.out.println(map.get("JiangXian"));
// size
System.out.println("=======size==========");
System.out.println("size=" + map.size());
// isEmpty
System.out.println("========isEmpty==========");
System.out.println("isEmpty=" + map.isEmpty());
// containsKey
System.out.println("=======contains==========");
System.out.println("contains=" + map.containsKey("邓超"));
// clear
System.out.println("========clear=======");
map.clear();
System.out.println("map=" + map);
System.out.println(map.isEmpty());
}
}
class Book{
private String name;
private int num;
public Book(String name, int num) {
this.name = name;
this.num = num;
}
}
Map接口遍历方式:
- 使用keySet——获得key的set后,用get去访问值;
- 使用values——直接获得Collection形式的值,遍历即可;
- 使用entrySet。
package com.jiangxian.map_.mapmethod_;
import java.util.*;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class MapFor_ {
public static void main(String[] args) {
Map map = new HashMap();
map.put("邓超","孙俪");
map.put("JiangXian",null);
map.put(null,"JiangXian");
map.put("hsp","hsp's wife");
// 使用HashMap的keySet,获取所有键
Set set = map.keySet();
// 1. 使用迭代器:
System.out.println("======keySet's iterator======");
Iterator iterator = set.iterator();
while(iterator.hasNext()){
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
// 2. 增强for
System.out.println("======keySet's for======");
for(Object key : set){
System.out.println(key + "-" + map.get(key));
}
// 使用HashMap的values:
Collection value = map.values();
// iterator
System.out.println("======values's iterator======");
Iterator iterator1 = value.iterator();
while(iterator1.hasNext()){
Object object = iterator1.next();
System.out.println(object);
}
// for
System.out.println("======values's for======");
for(Object o : value){
System.out.println(o);
}
// 使用entrySet获取所有关系的k-v
Set set1 = map.entrySet();
// iterator
System.out.println("======entrySet's iterator======");
Iterator iterator2 = set1.iterator();
while(iterator2.hasNext()){
Map.Entry entry = (Map.Entry)iterator2.next();
System.out.println(entry.getKey() + "-" + entry.getValue());
}
// for
System.out.println("======entrySet's for======");
for(Object o : set1){
Map.Entry entry = (Map.Entry)o;
System.out.println(entry.getKey() + "-" + entry.getValue());
}
}
}
Exercise:
package com.jiangxian.map_.exercise;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class Exercise {
public static void main(String[] args) {
Map map = new HashMap();
Employee jiangxian = new Employee("Jiangxian", 20000);
Employee Hsp = new Employee("Hsp", 30000);
Employee king = new Employee("King", 1000000000);
map.put(jiangxian.getId(), jiangxian);
map.put(Hsp.getId(), Hsp);
map.put(king.getId(), king);
System.out.println(map);
Set set = map.keySet();
// 输出薪水大于20000的,遍历方式1
for (Object key : set) {
Employee employee = (Employee) map.get(key);
if(employee.getSalary() > 20000){
System.out.println(employee);
}
}
// 遍历方式2
System.out.println("=======value========");
Collection value = map.values();
for(Object o : value){
Employee employee = (Employee) o;
if(employee.getSalary() > 20000){
System.out.println(employee);
}
}
// 遍历方式3:
Set set1 = map.entrySet();
System.out.println("=====entrySet======");
for(Object o : set1){
Map.Entry entry = (Map.Entry) o;
Employee employee = (Employee) entry.getValue();
if(employee.getSalary() > 20000){
System.out.println(employee);
}
}
}
}
class Employee{
public static int num;
private int id;
private String name;
private double salary;
public Employee(String name, double salary){
this.name = name;
this.salary = salary;
this.id = ++num;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
", salary=" + salary +
'}';
}
}
HashMap(Map接口实现类之一):
HashMap小结:
- HashMap 是 Map 接口使用频率最高的实现类;
- HashMap 是以 key-val 对的方式来存储数据(实际存放在HashMap$Node中)
- key 不能重复,但是value可以重复,允许使用null键和null值;
- 若添加相同的key,会用新的value覆盖掉原先旧的value;
- 与 HashSet 一样,不能保证映射的顺序,因为底层是 hash表的方式存储的(JDK8的hashMap 底层为数组+链表(+红黑树));
- HashMap 没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized。
底层机制和源码剖析:
扩容机制:
和HashSet相同(因为HashSet实际上创建的就是HashMap):
- HashMap 底层维护了Node类型的数组 table,默认为 null;
- 当创建对象时,将加载因子(loadFactor)初始化为0.75;
- 当添加k-v时,通过key的hash值,得到在 table 的索引位置。然后判断索引位置是否有元素:
- 若没有元素,直接添加;
- 若有元素,判断该元素的key和我们准备加入的key是否相等:
- 若相等,替换原始的value为新的value
- 若不相等,需要判断是树结构还是链表结构,做出相应的处理。(若容量不够还需要进行扩容)。
- 第一次添加后,table 的 capacity会被设置为默认的16,临界值 threshold 为 12;
- 以后每次扩容,都是将原始的 capacity * 2,临界值 threshold 也会变为原来的两倍;
- 在 JDK8 中,要满足以下两个条件,那么链表就会进化为树:
- 一条链表的长度超过了8;
- table 的大小 >= MIN_TREEIFY_CAPACITY(默认64)。
- 要是一条链表的长度>=8且还要往上添加节点,但是table的大小没有超过64,那么会执行的是扩容操作,哪怕没有达到阈值。
源码:
已经在 HashSet 和 Map 中分析过了,就不再啰嗦了。
HashTable(Map接口实现类之一):
基本介绍:
- 存放的是键值对k-v;
- HashTable 的键和值都不能是null,否则会抛出 NullPointerException的异常;
- HashTable 使用方法基本和 HashMap一样;
- HashTable 是线程安全的(synchronized),HashMap是线程不安全的。
与HashMap的对比:
版本 | 线程安全(同步) | 效率 | 允许null键和null值 | |
---|---|---|---|---|
HashMap | 1.2 | 不安全 | 较高 | 是 |
HashTable | 1.0 | 安全 | 较低 | 否 |
Properties(Map接口实现类之一):
基本介绍:
- Properties类继承自HashTable类并且实现了Map接口,也是一种键值对的形式来保存数据;
- 他的使用特点和Hashtable类似;
- Properties 还可以用于 从 xxx.properties文件中,加载数据到 Properties类对象,并进行读取和修改;
- 说明:工作后,常使用 xxx.properties 文件为配置文件,这个知识点会在IO流举例。
基本使用:
package com.jiangxian.map_;
import java.util.Properties;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class Properties_ {
public static void main(String[] args) {
// 1. Properties 继承 Hashtable
// 2. 可以通过 k-v 存放数据,当然 key 和 value 不能为 null
Properties properties = new Properties();
// properties.put("abc",null);
// properties.put(null,"def");
properties.put("lucy",100);
System.out.println("properties = " + properties);
}
}
基本用法外,在IO流中介绍。
总结——开发中如何选择集合实现类:
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
- 先判断存储的类型(一组对象[单列] 或 一组键值对[双列])
- 一组对象[单列]:Collection
- 允许重复:List
- 增删多:LinkedList [底层维护了一个双向链表];
- 改查多:ArrayList [底层维护 Object 类型的可变数组]。
- 不允许重复:Set
- 无序:HashSet——数组+链表(+红黑树);
- 排序:TreeSet;
- 插入和取出顺序一致:LinkedHashSet——数组+链表+双向链表;
- 允许重复:List
- 一组键值对[双列]:Map
- 键无序:HashMap
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap;
- 读取文件:Properties。
Collections 工具类:
Collections 工具类介绍:
- 是一个操作 Set、List 和 Map 等集合的工具类;
- 提供了一系列静态的方法对集合元素进行排序、查询和修改等操作。
排序操作:
- reverse(List):反转 List 中元素的顺序;
- shuffle(List):对 List 集合元素进行随机排序;
- sort(List):根据元素的自然顺序对指定 List 集合元素按升序排列;
- sort(List, Comparator):根据指定 Comparator 产生的顺序对List集合元素进行排序
- swap(List, int, int):将指定 List 集合中的 i 处元素和 j 处元素交换。
查找和替换:
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
- Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素;
- Object min(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素;
- int frequency(Collection, Object):返回指定集合中指定元素出现的次数;
- void copy(List dest, List src):将 src 中的内容复制到 dest 中;
- boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象中的所有旧值。
ity * 2,临界值 threshold 也会变为原来的两倍; - 在 JDK8 中,要满足以下两个条件,那么链表就会进化为树:
- 一条链表的长度超过了8;
- table 的大小 >= MIN_TREEIFY_CAPACITY(默认64)。
- 要是一条链表的长度>=8且还要往上添加节点,但是table的大小没有超过64,那么会执行的是扩容操作,哪怕没有达到阈值。
源码:
已经在 HashSet 和 Map 中分析过了,就不再啰嗦了。
HashTable(Map接口实现类之一):
基本介绍:
- 存放的是键值对k-v;
- HashTable 的键和值都不能是null,否则会抛出 NullPointerException的异常;
- HashTable 使用方法基本和 HashMap一样;
- HashTable 是线程安全的(synchronized),HashMap是线程不安全的。
与HashMap的对比:
版本 | 线程安全(同步) | 效率 | 允许null键和null值 | |
---|---|---|---|---|
HashMap | 1.2 | 不安全 | 较高 | 是 |
HashTable | 1.0 | 安全 | 较低 | 否 |
Properties(Map接口实现类之一):
基本介绍:
- Properties类继承自HashTable类并且实现了Map接口,也是一种键值对的形式来保存数据;
- 他的使用特点和Hashtable类似;
- Properties 还可以用于 从 xxx.properties文件中,加载数据到 Properties类对象,并进行读取和修改;
- 说明:工作后,常使用 xxx.properties 文件为配置文件,这个知识点会在IO流举例。
基本使用:
package com.jiangxian.map_;
import java.util.Properties;
/**
* @author JiangXian~
* @version 1.0
*/
@SuppressWarnings({"all"})
public class Properties_ {
public static void main(String[] args) {
// 1. Properties 继承 Hashtable
// 2. 可以通过 k-v 存放数据,当然 key 和 value 不能为 null
Properties properties = new Properties();
// properties.put("abc",null);
// properties.put(null,"def");
properties.put("lucy",100);
System.out.println("properties = " + properties);
}
}
基本用法外,在IO流中介绍。
总结——开发中如何选择集合实现类:
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
- 先判断存储的类型(一组对象[单列] 或 一组键值对[双列])
- 一组对象[单列]:Collection
- 允许重复:List
- 增删多:LinkedList [底层维护了一个双向链表];
- 改查多:ArrayList [底层维护 Object 类型的可变数组]。
- 不允许重复:Set
- 无序:HashSet——数组+链表(+红黑树);
- 排序:TreeSet;
- 插入和取出顺序一致:LinkedHashSet——数组+链表+双向链表;
- 允许重复:List
- 一组键值对[双列]:Map
- 键无序:HashMap
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap;
- 读取文件:Properties。
Collections 工具类:
Collections 工具类介绍:
- 是一个操作 Set、List 和 Map 等集合的工具类;
- 提供了一系列静态的方法对集合元素进行排序、查询和修改等操作。
排序操作:
- reverse(List):反转 List 中元素的顺序;
- shuffle(List):对 List 集合元素进行随机排序;
- sort(List):根据元素的自然顺序对指定 List 集合元素按升序排列;
- sort(List, Comparator):根据指定 Comparator 产生的顺序对List集合元素进行排序
- swap(List, int, int):将指定 List 集合中的 i 处元素和 j 处元素交换。
查找和替换:
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
- Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素;
- Object min(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素;
- int frequency(Collection, Object):返回指定集合中指定元素出现的次数;
- void copy(List dest, List src):将 src 中的内容复制到 dest 中;
- boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象中的所有旧值。