java的集合基础
一、集合的理解和好处
数组(灵活性不够)
1、长度开始时必须指定,而且一旦指定,不能更改
2、保存的必须为同一类型的元素
3、使用数组进行增加/删除元素比较麻烦
集合
1、可以动态的保存任意多个元素,使用方便,允许类型不一致
2、提供一系列方便的操作对象方法:add、remove、set、get等
3、使用集合添加/删除新元素,很简洁
二、两组集合关系图
单列集合 Collection
List : 有索引 存取有序 数据不唯一
ArrayList :数组数据结构 特点:查询快 增删慢
LinkedList: 双链表数据结构 特点:查询慢 增删快
Set : 没有索引 存取无序 数据唯一
HashSet :哈希表 = 数组 + 链表 + 二叉树
双列集合 Map
HashMap<K,V> 键唯一 值不唯一 哈希表控制键
向键位置存储对象所属类必须重写 hashCode equals方法
遍历map集合:
keySet 获取所有键
entrySet 获取键值对 Map.Entry3.
二、Collection接口和常用方法
1、基本介绍
1、collection实现子类可以存放多个元素,每个元素可以是Object
2、有些Collection的实现类可以存放重复元素,有些不可以
3、Collection的实现类有些是有序的(List),有些不是有序的(Set)
4、Collection接口没有实现子类,是通过它的子接口Set和List来实现的
2、常用方法
因为Collection接口本身是不可以实例化的,因此用实现子类ArrayList进行演示
import java.util.ArrayList;
import java.util.List;
public class CollectionMethod {
public static void main(String[] args) {
//创建ArrayList,使用接口接收
List list = new ArrayList();
//add:添加单个元素
list.add("java"); //字符型
list.add(10); //整型 相当于list.add(new Integer(10))
list.add(true); //同上
//现在输出的就是一个一个对象 [java,10,true]
System.out.println("add: list = " + list);
//remove:删除指定元素
list.remove(0);//根据下标删除
list.remove("java");//根据指定对象删除,删除指定元素
System.out.println("remove: list = " + list);
//contains:查找元素是否存在
System.out.println("contains: list = " + list.contains("java"));
//size:返回元素的个数
System.out.println("size: list = " + list.size());
//isEmpty:判断是否为空
System.out.println("isEmpty: list = " + list.isEmpty());
//clear:清空
list.clear();
System.out.println("clear: list = " + list);
//addAll:添加多个元素(可传入一个集合)
ArrayList arrayList = new ArrayList();
arrayList.add("红楼梦");
arrayList.add("三国演义");
list.addAll(arrayList);
System.out.println("addAll: list = " + list);
//containsAll:查找多个元素是否存在(可传入一个集合)
System.out.println("containsAll: list = " + list.containsAll(arrayList));
//removeAll:删除多个元素(可传入一个集合)
list.removeAll(arrayList);
System.out.println("removeAll: list = " + list);
}
}
3、遍历方式
1、使用迭代器Iterator
基本介绍
1、Iterator对象成为迭代器,主要用于遍历Collection集合中的元素
2、所实现了Collection接口的集合类都有一个 iterator() 方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器
3、Iterator的结构
4、Iterator仅用于遍历集合,Iterator本身不存放对象
迭代器原理
迭代器代码
//得到一个集合的迭代器
Iterator iterator = list.iterator();
//判断集合是否还有元素,没有返回false,退出循环
while (iterator.hasNext()){
//打印元素
//next() 1、指针下移 2、将下移以后集合位置上的元素返回
System.out.println(iterator.next());
}
迭代器原理图
注意:在调用it.next()方法之前必须调用it.hasNext()进行检测,如果不调用,且下一条记录无效时,直接调用it.next()方法会抛出NoSuchElementException异常
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class CollectionIterator {
public static void main(String[] args) {
Collection collection = new ArrayList();
collection.add(new Book("三国演义","罗贯中",10.1));
collection.add(new Book("红侯梦","曹雪芹",9.1));
collection.add(new Book("三体","刘慈欣",12.1));
//遍历集合
//1、得到集合的迭代器
Iterator iterator = collection.iterator();
//2、使用while进行遍历
while (iterator.hasNext()){
//返回的是object,因为add添加的是一个object
Object obj = iterator.next();
//会去调用toString方法
//object的编译类型是object,运行类型取决于存放的对象,这里存放的是Book对象,所以根据动态绑定机制回去调用toString方法
System.out.println("obj = " + obj);
}
//3、当退出while退出循环后,这时iterator迭代器指向最后一个元素
//iterator.next(); //异常 java.util.NoSuchElementException
//4、需要重新遍历,需要重置迭代器
iterator = collection.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj = " + obj);
}
}
}
class Book{
private String bookName;
private String author;
private double price;
public Book(String bookName, String author, double price) {
this.bookName = bookName;
this.author = author;
this.price = price;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
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;
}
public Book() {
}
@Override
public String toString() {
return "Book{" +
"bookName='" + bookName + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}
2、增强for循环
增强for循环,可以代替iterator迭代器,特点:增强for就是简化版的iterator,本质一样,只适用于遍历集合和数组
基本语法:
for( 元素类型 元素名 : 集合名或者数组名 ){
访问对象
}
增强for的底层,在ArrayList的源码下
//源码,返回一个迭代器
public Iterator<E> iterator() {
return new Itr();
}
//源码,调用hasNext判断是否是最后一个元素
public boolean hasNext() {
return cursor != size;
}
//源码,使用next()方法进行遍历
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
import java.util.ArrayList;
import java.util.Collection;
public class CollectionFor {
public static void main(String[] args) {
Collection collection = new ArrayList();
collection.add(new Book1("三国演义","罗贯中",10.1));
collection.add(new Book1("红侯梦","曹雪芹",9.1));
collection.add(new Book1("三体","刘慈欣",12.1));
//1、本质与iterator基本一致
//2、底层也是迭代器
//3、可以理解为简化版的迭代器遍历
for (Object o : collection) {
System.out.println(o);
}
//数组使用
int[] ints = {1,2,3,4,5,6,7};
for (int anInt : ints) {
System.out.println(anInt);
}
}
}
class Book1{
private String bookName;
private String author;
private double price;
public Book1(String bookName, String author, double price) {
this.bookName = bookName;
this.author = author;
this.price = price;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
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;
}
public Book1() {
}
@Override
public String toString() {
return "Book{" +
"bookName='" + bookName + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}
3、迭代器和增强for循环练习
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* 练习
* 创建三个dog对象{name,age},放入ArrayList中,赋给List引用,
* 用迭代器和增强for循环进行遍历
* 重写toString方法,输出name和age
*/
public class CollectionExercise {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Dog("旺财",10));
list.add(new Dog("狗蛋",12));
list.add(new Dog("大白",8));
list.add(new Dog("小黑",4));
//1、使用迭代器进行遍历
System.out.println("=============迭代器============");
//获取迭代器
Iterator iterator = list.iterator();
//判断是否还有元素
while (iterator.hasNext()) {
Object obj = iterator.next();
//输出元素
System.out.println("obj = " + obj);
}
//2、使用增强for循环进行遍历
System.out.println("==============增强for循环==========");
for (Object obj : list) {
System.out.println("obj = " + obj);
}
}
}
class Dog{
private String name;
private Integer age;
public Dog(String name, Integer age) {
this.name = name;
this.age = age;
}
public Dog() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
三、List接口和用方法
1、基本介绍
List接口是Collection接口的子接口
1、List集合类中元素有序(即添加顺序和取出顺序一致),且元素可重复
//1、List集合类中元素有序(即添加顺序和取出顺序一致),且元素可重复
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
System.out.println("list = "+list);
//结果
list = [1, 2, 3]
2、List集合中的每个元素都有对应的顺序索引,即支持索引
//2、List集合中的每个元素都有对应的顺序索引,即支持索引
//索引从0开始
System.out.println(list.get(0));
3、List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
4、JDK API中List接口的常用实现类有 ArrayList、LinkedList和Vector
2、常用方法
import java.util.ArrayList;
import java.util.List;
public class ListMethod {
public static void main(String[] args) {
List list = new ArrayList();
list.add("张三丰");
list.add("孙悟空");
//list的常用方法
//void add(int index, Object obj) 在index位置插入obj,在没有加上索引,则会插入最后一个元素
//接口源码 void add(int index, E element);
list.add(1,"刘备");
System.out.println("add: list = " + list);
//boolean addAll(int index, Collection col); 在index位置开始将col中的所有元素进行添加
//接口源码 boolean addAll(int index, Collection<? extends E> c);
List arrayList = new ArrayList();
arrayList.add("喜洋洋");
arrayList.add("美羊羊");
arrayList.add("懒洋洋");
arrayList.add("刘备");
list.addAll(1,arrayList);
System.out.println("addAll: list = " + list);
//Object get(int index): 根据索引获取对应的元素
//接口源码 E get(int index);
System.out.println("get: list = " + list.get(1));
//int indexOf(Object obj): 返回obj在集合中首次出现的位置
//接口源码 int indexOf(Object o)
System.out.println("indexOf: list = " + list.indexOf("刘备"));
//int lastIndexOf(Object obj); : 返回obj在集合中末次出现的位置
//接口源码int lastIndexOf(Object o);
System.out.println("lastIndexOf: list = " + list.lastIndexOf("刘备"));
//Object remove(int index): 移除指定index位置的元素,并返回此元素
//接口源码 E remove(int index);
System.out.println(("remove: list = " + list.remove(1)));
//Object set(int index, Object obj): 设置指定index位置的元素为obj,相当于替换,前提是指定的索引必须存在,否则会抛出下标越界异常
//接口源码 E set(int index, E element);
list.set(1,"牛洋洋");
System.out.println("set: list = " + list);
//List subList(int fromIndex, int toIndex): 返回fromIndex到toIndex位置的子集合
//接口源码 List<E> subList(int fromIndex, int toIndex);
System.out.println("subList: list = " + list.subList(0,2));
}
}
3、List练习
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* 添加10个以上元素(比如 String "hello"),在2号位插入一个 喜洋洋
* 获取第5个元素,删除第6个元素,修改第7个元素,再使用迭代器 进行遍历集合
* 要求:使用List实现类ArrayList完成
*/
public class ListExercise {
public static void main(String[] args) {
List list = new ArrayList();
for (int i = 0; i < 12; i++) {
list.add("hello" + i);
}
System.out.println("list = " + list);
//在2号位添加一个 喜洋洋
System.out.println("============在2号位添加一个 喜洋洋============");
list.add(1,"喜洋洋");
System.out.println("list = " + list);
//获取第5个元素
System.out.println("============获取第5个元素============");
System.out.println("list = " + list.get(4));
//删除第6个元素
System.out.println("============删除第6个元素============");
list.remove(5);
System.out.println("list = " + list);
//修改第7个元素
System.out.println("============修改第7个元素============");
list.set(6,"懒洋洋");
System.out.println("list = " + list);
System.out.println("============使用迭代器遍历============");
Iterator iterator = list.iterator();
while (iterator.hasNext()){
Object obj = iterator.next();
System.out.println("obj = " + obj);
}
}
}
4、List的三种遍历方式
只要是List接口的实现子类(ArrayList、Vector、LinkedList),遍历方式一致
-
方式一:使用iterator方式
//迭代器 Iterator iterator = list.iterator(); while (iterator.hasNext()){ Object object = iterator.next(); System.out.println("object = " + object); }
-
方式二:增强for循环
//增强for for (Object object : list) { System.out.println("object = " + object); }
-
方式三:使用普通的for
//普通for for (int i = 0; i < list.size(); i++) { System.out.println("object = " + list.get(i)); }
5、练习,冒泡排序
创建Book对象
public class Book {
private String name;
private String author;
private double price;
public Book() {
}
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 "名称:" + name + "\t\t价格:" + price + "\t\t作者:" + author;
}
}
进行测试
import java.util.ArrayList;
import java.util.List;
public class ListExercise02 {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Book("天龙八部","金庸",10));
list.add(new Book("三体","刘慈欣",50));
list.add(new Book("龙族","江南",20));
list.add(new Book("西游记","吴承恩",30));
//遍历
for (Object o : list) {
System.out.println(o);
}
//调用冒泡排序
sort(list);
System.out.println("===============排序过后=================");
//遍历
for (Object o : list) {
System.out.println(o);
}
}
//冒泡排序方法
public static void sort(List list){
int size = list.size();
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - 1 - i; j++) {
//取出对象Book,向下转型
Book book1 = (Book) list.get(j);
Book book2 = (Book) list.get(j + 1);
//比较交换
if (book1.getPrice() > book2.getPrice()){
//如果book1的价格比book2大,那么这两个位置的元素进行交换,使用set方法进行交换
list.set(j, book2);
list.set(j + 1, book1);
}
}
}
}
}
6、ArrayList底层结构和源码分析
ArrayList的注意事项:
-
permits all elements,including null ,ArrayList可以放入null,并且多个
ArrayList arrayList = new ArrayList(); arrayList.add(null); arrayList.add(null); arrayList.add(null); arrayList.add(null); arrayList.add("喜洋洋"); System.out.println(arrayList);
-
ArrayList是由数组来实现数组存储的
-
ArrayList基本等同与Vector,除了ArrayList是线程不安全(执行效率高),在多线程情况下,不建议使用ArrayList
//源码 没有加 synchronized, 没有做线程互斥 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
扩容机制
1、ArrayList中维护了一个Object类型的数组elementData, transient Object[] elementData
- Object是一个所有类的父类,什么类型都可以往里面放
- transient表示短暂的,瞬间的,如果有个属性被 transient修饰后,表示该属性不会被序列化
2、当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData的容量为0,第一次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍
3、如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如需要再次扩容,则直接扩容elementData为1.5倍
源码分析:
测试代码:
import java.util.ArrayList;
/**
* ArrayList源码分析
*/
public class ArrayListSource {
public static void main(String[] args) {
//无参构造
ArrayList list = new ArrayList();
//有参构造
//ArrayList list = new ArrayList(8);
//首次添加
for (int i = 0; i <= 10; i++) {
list.add(i);
}
//第二次扩容
for (int i = 11; i <= 15 ; i++) {
list.add(i);
}
//第三次扩容
list.add(100);
list.add(200);
list.add(null);
}
}
无参构造分析源码:
//无参构造
ArrayList list = new ArrayList();
第一步:ArrayList第一次初始化的就是创建一个空数组
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
//创建空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//空数组定义
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
第二步:执行添加的方法,在添加元素之前,需要进行判断是否要扩容
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
//执行添加元素之前,先确定数组容量是否够,不够就先扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//容量足够之后再进行添加操作
elementData[size++] = e;
return true;
}
第三步:确定minCapacity最小容量,第一次扩容为10
//该方法用于判断是否扩容
private void ensureCapacityInternal(int minCapacity) {
//这里明确集合是否是真的需要扩容
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//源码
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//判断传进来的数组是否等于空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//不等于就开始进行比较,取最大值
//DEFAULT_CAPACITY的默认值为10
//minCapacity目前集合添加的第几个元素
//如果默认值大于minCapacity,就返回默认的长度
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//不满足条件则返回需要的容量大小 minCapacity
return minCapacity;
}
第四步:记录当前集合被修改的次数,判断最小容量是否足够,不够就执行扩容方法,每次添加元素都会执行该方法判断是否要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //记录当前集合被修改的次数,每次添加一个元素记录一次
// overflow-conscious code
//使用当前最小容量 减去 数组的长度,如果大于0,说明容量大小真的不够了,直接调用扩容
if (minCapacity - elementData.length > 0)
//进行扩容处理
grow(minCapacity);
}
第五步:进行扩容;使用扩容机制确定扩容大小;第一次newCapacity = 10, 第二次以及以后按照1.5倍扩容;扩容使用的是Arrays.copyOf(),使用Arrays.copyOf()的保留之前的数据,然后再进行扩容
/**
* 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
*/
private void grow(int minCapacity) {
// overflow-conscious code
//将当前数组的长度记录到旧的容量变量中(oldCapacity)
int oldCapacity = elementData.length;
//计算新的容量,使用旧的长度(oldCapacity)加上旧的长度的(oldCapacity)1/2进行扩容,也就是1.5倍,就得到新的容量
//例如 oldCapacity = 10,则 newCapacity = 10 + 10 *(1/2) = 15
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新的长度减去最小长度小于0,那么最小长度就为新的长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新的长度减去规定的最大长度,就调用hugeCapacity()方法重新给新的长度赋值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//进行扩容,elementData生成扩容的新数组,填充null,
//Arrays.copyOf(elementData, newCapacity);再给数组赋值
elementData = Arrays.copyOf(elementData, newCapacity);
}
第六步:扩容完成后,返回到添加方法
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
//扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//扩容完毕后,进行赋值
//size++是先赋值后再进行++操作,确保下标为0之前进行赋值,再向下移动下标
elementData[size++] = e;
return true;
}
有参构造分析源码:
//有参构造
ArrayList list = new ArrayList(8);
第一步:对传入的参数进行判断,创建指定大小的elementData数组 new Object[initialCapacity]
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
//判断传入的参数是否大于0
if (initialCapacity > 0) {
//大于0的参数,创建一个指定大小的Object[initialCapacity]
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果传入的是0,那么就还是按照无参构造器进行使用,从0开始创建一个大小为10的数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//小于0的话,直接抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
第二步:与无参构造的执行流程一致,进行比较扩容
小结:如果是有参的构造器,那么第一次扩容就是按照elementData的1.5倍扩容,不会是默认的10个容量大小
7、Vector的底层结构和源码剖析
基本介绍
1、继承关系
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
2、Vector底层也是对象数组
protected Object[] elementData;
3、Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized
源码例子
//源码带有synchronized关键字修饰
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
4、在开发中,需要线程同步安全时,考虑使用Vector
Vector与ArrayList比较
底层结构 | 版本 | 线程安全(同步)效率 | 扩容倍数 | |
---|---|---|---|---|
ArrayList | 可变数组 | jdk1.2 | 不安全,效率高 | 如果是有参构造,那么就是1.5倍;如果是无参构造,第一次是10,第二次以及之后是1.5倍扩容 |
Vector | 可变数组 | jdk1.0 | 安全,效率不高 | 如果指定大小,每次直接是2倍扩容;如果是无参构造,默认是10,满后,按照2倍扩容 |
源码解读:
//创建集合
Vector vector = new Vector();
for (int i = 0; i < 10; i++) {
vector.add(i);
}
第一步:构建无参构造器,实际上是执行了有参构造器,默认是10
//源码,默认给10的容量,实际上调用的是有参构造器,默认长度是10
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
第二步:添加数据
public synchronized boolean add(E e) {
modCount++;//记录集合的修改次数
ensureCapacityHelper(elementCount + 1); //判断扩容
elementData[elementCount++] = e;//给集合赋值
return true;
}
第三步:判断是否需要真的扩容
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
//判断是否需要真正的扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);//扩容处理
}
第四步:执行扩容机制,计算扩容的算法与ArrayList不太一致,Vector使用的是2倍扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//自动增量 capacityIncrement,默认为0,三元运算符,返回的是oldCapacity
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);
}
8、LinkedList的底层结构和源码分析
基本介绍:
1、LinkedList底层实现双向链表和双端队列特点
2、可以添加任意元素(元素可以重复),包括null
3、线程不安全,没有实现线程同步
LinkedList是底层操作机制
1、LinkedList底层维护的是一个双向链表
2、LinkedList中维护了两个属性first和last分别指向首节点和尾结点
3、每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终实现双向链表
4、所以LinkedList的元素的添加和删除,不是通过数组实现的,相对来说效率较高。
5、双向链表的模拟实现
/**
* 模拟一个简单的双向链表
*/
public class LinkedListNode {
public static void main(String[] args) {
//创建节点
Node jack = new Node("jack");
Node tom = new Node("tom");
Node lucy = new Node("lucy");
//连接三个节点,形成双向链表
//下面的代码形成的关系 jack -> tom -> lucy
jack.next = tom;
tom.next = lucy;
//下面的代码形成的关系 jack <- tom <- lucy
lucy.pre = tom;
tom.pre = jack;
//让first引用指向jack,就是双向链表的首节点
Node first = jack;
//让last引用指向lucy,就是双向链表的尾节点
Node last = lucy;
System.out.println("==============演示(从头到尾的输出)=============");
//演示从头到尾遍历
while (true){
//如果first(首节点为空,则说明遍历到了最后,退出循环)
if (first == null){
break;
}
//打印首节点指向的元素
System.out.println(first);
//首节点移至下一位,再次循环判断打印
first = first.next;
}
System.out.println("==============演示(从尾到尾到头的输出)=============");
//从尾到头的遍历(原理同上一直)
while (true){
if (last == null){
break;
}
System.out.println(last);
last = last.pre;
}
//演示,双向链表的添加数据
//在tom-----------lucy中添加一个元素(喜洋洋)
//1、创建一个Node节点
Node xyy = new Node("喜洋洋");
//2、让 xyy 的 next 指向lucy,让 xyy 的 pre 指向 tom
xyy.next = lucy;
xyy.pre = tom;
//3、让 lucy 的 pre 指向 xyy,让 tom 的 next 指向 xyy
lucy.pre = xyy;
tom.next = xyy;
//first重新指向首位
first = jack;
System.out.println("==============添加元素后的遍历=============");
//演示从头到尾遍历
while (true){
//如果first(首节点为空,则说明遍历到了最后,退出循环)
if (first == null){
break;
}
//打印首节点指向的元素
System.out.println(first);
//首节点移至下一位,再次循环判断打印
first = first.next;
}
}
}
//定义一个Node类,Node对象表示双向链表的节点,可以被实例化
class Node{
public Object item; //真正存放数据的地方
public Node next;
public Node pre;
public Node(Object item) {
this.item = item;
}
@Override
public String toString() {
return "Node name = " + item;
}
}
LinkedList的源码图解
添加节点的源码:
第一步:
//创建LinkedList
LinkedList linkedList = new LinkedList();
//执行添加
linkedList.add(1);
这时会调用一个无参构造的方法,初始的属性 first = null,last = null,size = 0
//首先会调用无参构造方法(源码)
public LinkedList() {}
第二步:执行add方法
//源码
public boolean add(E e) {
linkLast(e);
return true;
}
linkLast方法的实现,当只有一个元素时,first 和 last 都指向这个节点,元素都是从尾进行插入的,因此刚刚插入的数据都是赋值给last
当第一次添加数据,last = null,就会把新的元素赋值给 last ,根据 if (l == null) ,也将第一个元素赋值给 first
第二次插入元素,last不为空,因为之前已经有一个数据了,所以这个 l.next 指向下一个元素
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null); //赋值,因为last元素没有next,也就是没有下一个元素,所以next为null
last = newNode; //第一个节点,last指向这个节点
if (l == null)
first = newNode;
else
l.next = newNode;
size++; //长度加1
modCount++; //修改次数加1
}
删除第一个节点的源码:
//默认删除的是第一个节点
linkedList.remove();
第一步:调用删除第一个元素的方法
//源码(执行了删除第一个的方法)
public E remove() {
return removeFirst();
}
第二步:判断第一个元素的first是否为空 first == null
//源码
public E removeFirst() {
final Node<E> f = first; //将第一个元素的first赋值给 f
if (f == null) //如果f为空,直接抛出异常
throw new NoSuchElementException();
return unlinkFirst(f); //不为空调用unlinkFirst方法,传入f
}
第三步:执行真正的删除方法
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item; //首先将第一个元素item赋值给element
final Node<E> next = f.next; //再将第一个元素的next赋值给next
f.item = null; //设置第一个元素的item为null
f.next = null; // help GC 设置第一个元素的next设置为null
first = next; //第二行已经给next赋值了,是第二个元素的值,也就是将fist指向第二个元素
if (next == null) //所以这时的next不是空,而是第二个元素
last = null; //跳过这个设置last为null的判断,这时的last是第三个元素
else
next.prev = null; //设置第二个元素的后指向pre为null
size--; //长度减1
modCount++; //修改次数加1
return element; //返回被删除的元素
}
9、ArrayList和LinkedList的比较
底层结构 | 增删的效率 | 改查的效率 | |
---|---|---|---|
ArrayList | 可变数组 | 较低,数组扩容 | 较高 |
LinkedList | 双向链表 | 较高,通过链表追加 | 较低 |
如何选择ArrayList和LinkedList:
1、如果我们改查操作多,选择ArrayList
2、如果我们增删操作多,选择LinkedList
3、一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList
4、在一个项目中,根据业务进行灵活选择,也可能这样,一个模块使用ArrayList,另一个模块使用LinkedList
四、Set接口的使用方法
基本介绍:
1、无序(添加和取出的顺序不一致),没有索引
2、不允许重复的元素,所以最多包含一个null
3、jdk API 中Set接口的实现类有HashSet,TreeSet等
-
Set的常用方法:和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样
-
Set接口的遍历方式
同Collection的遍历方式一样,因此Set接口是Collection接口的子接口
1、可以使用迭代器
2、使用增强for
3、不能使用索引的方式来获取
基本演示
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
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("jack");
set.add("tom");
set.add("lucy");
set.add(null);
set.add(null);
System.out.println("set = " + set);
//遍历,使用迭代器
System.out.println("---------------迭代器----------------");
Iterator iterator = set.iterator();
while (iterator.hasNext()){
Object obj = iterator.next();
System.out.println("obj = " + obj);
}
//遍历,使用增强for
System.out.println("---------------增强for----------------");
for (Object obj : set) {
System.out.println("obj = " + obj);
}
}
}
1、HashSet的说明
1、HashSet实现了Set接口
2、HashSet实际上是HashMap
//构造器
Set set = new HashSet();
//源码
public HashSet() {
map = new HashMap<>();
}
3、可以存放null值,但是只能有一个null
4、HashSet不保证元素时有序的,取决于hash后,确定索引的结果(不保证存放元素的顺序和取出的顺序一致)
5、不能存放重复元素/对象
演示
import java.util.HashSet;
import java.util.Set;
public class HashSet01 {
public static void main(String[] args) {
Set set = new HashSet();
//1、在执行add方法后,会返回一个boolean值
//2、如果添加成功,返回true,否则返回false
System.out.println(set.add("jack"));
System.out.println(set.add("john"));
System.out.println(set.add("tom"));
System.out.println(set.add("jack"));
System.out.println(set.add("lucy"));
System.out.println(set.add("mary"));
//3、通过remove指定删除一个对象
set.remove("jack");
//输出的顺序不保证与添加的顺序一致
System.out.println("set = " + set);
//重置set集合
set = new HashSet();
System.out.println("重置set = " + set);
set.add("lucy"); //添加成功
set.add("lucy");//添加失败
//两个都添加成功,并且不重复
set.add(new Dog("tom"));
set.add(new Dog("tom"));
System.out.println("添加set = " + set);
//经典面试题,这两个是否添加成功而且不重复
set.add(new String("java"));
set.add(new String("java"));
//结论,只添加了一个java,原理需要看源码的add
System.out.println("面试set = " + set);
}
}
class Dog{
private String name;
public Dog(String name) {
this.name = name;
}
}
2、数组链表的模拟
HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
/**
* 需要debug去了解
* 尤其是挂载的位置,next的指向会将这个数据结构变成一个链表
* 每个table的索引上可能会有一条链表
*/
public class HashSetStructure {
public static void main(String[] args) {
//模拟一个HashSet的底层结构(也就是HashMap的底层结构)
//1、创建一个数组,数组类型是Node[]
//2、有些人直接把Node[]数组称为表
Node[] table = new Node[16];
System.out.println("Node = " + table);
//创建节点
Node john = new Node("john", null);
//把节点放到索引为2的位置
table[2] = john;
//再创建一个节点
Node jack = new Node("jack", null);
//将jack挂载到john后面(也就是将jack加到john后面)
john.next = jack;
//再创建一个节点
Node rose = new Node("rose", null);
//将rose节点挂载到jack节点后面
jack.next = rose;
//将lucy放到索引为3的位置
Node lucy = new Node("lucy", null);
table[3] = lucy;
System.out.println("Node = " + table);
}
}
//节点,存储数据,可以指向下一个节点,从而形成链表
class Node{
Object item; //存放数据
Node next; //指向下一个节点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
3、HashSet扩容解析
结论:
1、HashSet底层是HashMap
2、添加一个元素时,先得到hash值,hash值会转为索引值
3、找到存储数据表table,看到这个索引位置是否已经存放有数据
4、如果没有,则直接存放数据
5、如果有,会调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后
6、java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEEIFY_CAPACITY(默认64),就会进行树化(红黑树)
4、源码解读
第一步:创建HashSet,走的是构造器,HashMap
HashSet set = new HashSet();
//源码
public HashSet() {
map = new HashMap<>();
}
第一次,添加add源码
第二步:添加数据,执行add
set.add("java");
//源码
public boolean add(E e) {
// private static final Object PRESENT = new Object();
//PRESENT是起到占位作用,是一个静态不可改变的Object对象
//e是传进来的值
return map.put(e, PRESENT)==null; //底层的方法,返回的是null,则代表已经添加成功了
}
第三步:执行的是put方法,该方法会执行 hash(key) 得到key对应的hash值,但是得到的不是并不是key真正的 hashCode,为了降低hash冲突的几率,执行了一个算法
//源码
//key是传进来的值
//value是PRESENT,共享的
public V put(K key, V value) {
//hash(key)计算出hash值,确定数据在表里面的索引位置
return putVal(hash(key), key, value, false, true);
}
//源码,计算hash值,key是传进来的元素
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
第四步:执行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; //辅助变量
//table 就是HashMap的一个数组,类型是Node[]
if ((tab = table) == null || (n = tab.length) == 0) //第一次添加初始状态table为空,长度等于0
n = (tab = resize()).length; //经过扩容,resize(),提供了16个空间
//1、根据key,得到hash 去计算这个key应该存放在table表的哪个索引位置,并且把这个位置的对象,赋值给 p
//2、再判断这个p是否为空,如果p为空,则表示该位置没有存放元素,就创建 Node(key,value),其中这个key就是元素,value是占位PRESENT,最后一个null是next,指向下一个元素,hash是为了比较下一个存进来的值的hash是否相等
//3、最后将这个 Node newNode(hash, key, value, null),放在 tab[i] 位置
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//threshold 通过resize()方法计算出的临界容量
if (++size > threshold)
resize(); //如果size大于临界容量,那么就开始扩容
afterNodeInsertion(evict); //空方法
return null; //代表成功了
}
第二次,添加重复元素
添加调用的方法,与第一次一致,但是执行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; //声明辅助变量
//如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
//并且满足下面两个条件之一:
//(1)准备加入的key和p指向的Node结点的key是同一个对象
//(2)p指向的Node结点的key的equals()和准备加入的key比较相同
//则不能加入
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//再判断p是不是一棵红黑树
//是的话就调用putTreeVal进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果table对应位置,已经是个链表,那么就使用for循环比较
//(1)依次和该链表的每个元素比较后,都不相同,则加入到该链表最后
//注意:在把元素添加到链表后,立即判断该链表是否已经到达8个节点,到达8个就调用treeifyBin()对当前这个链表进行树化(转成红黑树)
//注意:在转成红黑树前需要进行判断,判断条件如下
//tab不为null并且tab的长度不超过64
//if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//resize();
//如果上述条件成立,先进行table扩容,不成立再进行树化
//(2)依次和该链表的每个元素比较后,如果存在一个相同的,直接break
for (int binCount = 0; ; ++binCount) {//循环比较
//e是链表的第二个元素,p是第一个元素,依次类推,e是第三个元素时,则p是第二个元素
//判断e是否为null,如果是,则直接添加
if ((e = p.next) == null) {
// 链表中,给p的下一个值赋值
p.next = newNode(hash, key, value, null);
//判断是否需要进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//不是null,进行比较,如果链表上的值与要添加进来的值一致,直接break
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//再将e的值赋值给p,进行e后移一个元素
}
}
//判断这个e是否为空,不为空则返回的是添加进来的值,说明添加失败
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//size,每加入一个节点Node,就会增加1
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
5、HashSet的扩容和转成红黑树机制
结论:
1、HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor = 0.75)= 12
2、如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,以此类推
3、在java8中,涂过一条链表的元素个数达到了 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制
演示代码:
public class HashSetIncrement {
public static void main(String[] args) {
HashSet set = new HashSet();
//1、HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor = 0.75)= 12
//2、如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,以此类推
//添加
for (int i = 0; i <= 100; i++) {
set.add(String.valueOf(i));
}
//3、在java8中,涂过一条链表的元素个数达到了 TREEIFY_THRESHOLD(默认是8),
// 并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),
// 否则仍然采用数组扩容机制
//如何将元素放到同一个链表上,首先保证hash值相同,但是值的内容不同
for (int i = 0; i <= 12; i++) {
set.add(new A(i)); //保证hash一致
}
System.out.println("set = " + set);
}
}
class A{
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 100;
}
}
疑问:
set集合中size是否是只是计算链表的第一个元素,还是计算table 中的所有元素?
测试:
import java.util.HashSet;
import java.util.Objects;
public class HashSetIncrement {
public static void main(String[] args) {
HashSet set = new HashSet();
/**
* 判断size是否只是计算table中链表的第一个元素,还是计算所有的元素
* 在添加元素时,底层会将元素封装为一个Node,再加入到table中,就算增加一个元素,size++,不只是table中链表的第一个元素
*/
//先给table中的某条链表添加7个A对象
for (int i = 0; i <= 7; i++){
set.add(new A(i)); //加在同一条链表上
}
//再给table的另一条链表添加7个B对象
for (int i = 0; i <= 7; i++) {
set.add(new B(i)); //加在同一条链表上
}
}
}
class B{
private int n;
public B(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 200;
}
}
class A{
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 100;
}
}
结论:
1、在添加元素时,底层会将元素封装为一个Node,再加入到table中,就算增加一个元素,size++,不只是table中链表的第一个元素
2、即使table没有被填满,但是当size到达临界值,也会触发扩容机制,进行扩容
6、HashSet练习
第一题:
要求:
1、定义一个Employee类,该类包含private成员属性name,age
2、创建3个Employee对象,放入HashSet中
3、当name和age值相同时,人为是相同员工,不能添加到HashSet集合中
import java.util.HashSet;
import java.util.Objects;
public class HashSetExercise {
public static void main(String[] args) {
HashSet set = new HashSet();
//经过重写equals和hashCode后,当名字和年龄相同后,hashSet就会认为添加的元素相同,加入不成功
set.add(new Employee("tom",18));
set.add(new Employee("jack",19));
set.add(new Employee("tom",18));
System.out.println("set = " + set);
}
}
class Employee{
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public Employee() {
}
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;
}
@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);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
第二题:
要求:
1、定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中birthday为MyDate类型(属性包括:year,month,day)
2、创建3个Employee放入HashSet中
3、当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
import java.util.HashSet;
import java.util.Objects;
public class HashSetExercise02 {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add(new Employee02("tom",12,new MyData(2012,12,5)));
set.add(new Employee02("jack",12,new MyData(2012,12,5)));
set.add(new Employee02("tom",12,new MyData(2012,12,5)));
System.out.println("set = " + set);
}
}
class Employee02{
private String name;
private int sal;
private MyData birthday;
public Employee02() {
}
public Employee02(String name, int sal, MyData birthday) {
this.name = name;
this.sal = sal;
this.birthday = birthday;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSal() {
return sal;
}
public void setSal(int sal) {
this.sal = sal;
}
public MyData getBirthday() {
return birthday;
}
public void setBirthday(MyData birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "Employee02{" +
"name='" + name + '\'' +
", sal=" + sal +
", birthday=" + birthday +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee02 that = (Employee02) o;
return sal == that.sal &&
Objects.equals(name, that.name) &&
Objects.equals(birthday, that.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, sal, birthday);
}
}
class MyData{
private int year;
private int month;
private int day;
public MyData(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public MyData() {
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
@Override
public String toString() {
return "MyData{" +
"year=" + year +
", month=" + month +
", 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 year == myData.year &&
month == myData.month &&
day == myData.day;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
}
7、LinkedHashSet
介绍:
1、LinkedHashSet是HashSet的子类
2、LinkedHashSet底层是一个LinkedHashMap,底层维护一个 数组 + 双向链表
3、LinkedHashSet根据元素的HashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的
4、LinkedHashSet不允许有重复元素
说明:
1、在LinkedHashSet中维护了一个hash表和双向链表(LinkedHashSet 有 head 和 tail)
2、每一个节点有 before 和 after 属性,这样可以形成双向链表
3、在添加一个元素时,先求hash值,在求索引,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加【原则和HashSet一样】)
tail.next = newElement //简单指定
newElement.pre = tail
tail = newElement;
4、这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致
分析底层:
检测代码:
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetSource {
public static void main(String[] args) {
Set set = new LinkedHashSet();
set.add(new String("AA"));
set.add(456);
set.add(456);
set.add(new Customer("漩涡鸣人",1001));
set.add(123);
set.add("HELLO");
System.out.println("set = " + set);
}
}
class Customer{
private String name;
private int num;
public Customer() {
}
public Customer(String name, int num) {
this.name = name;
this.num = num;
}
@Override
public String toString() {
return "Customer{" +
"name='" + name + '\'' +
", num=" + num +
'}';
}
}
解读:
1、LinkedHashSet 添加和取出元素/数据的顺序是一致的
2、LinkedHashSet底层维护的是一个LinkedHashMap(是HashMap的子类)
3、LinkedHashSet底层结构(数组table + 双向链表)
4、添加第一次时,直接将数组table扩容到16,存放的接待您类型是LinkedHashMap$Entry
5、数组时HashMap$Node[] 存放的元素 / 数据是LinkedHashMap$Entry(数组多态,Entry继承了HashMap的Node,继承关系是在内部类进行完成的)
6、底层的添加方法还是走的HashSet添加方法
add()源码:
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;
}
}
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;
}
练习:
要求:
Car类(属性:name,price),如果name和price一样,则认为是相同元素,就不添加
需要重写equals方法和hashCode方法
import java.util.LinkedHashSet;
import java.util.Objects;
public class LinkedHashSetExercise {
public static void main(String[] args) {
LinkedHashSet set = new LinkedHashSet();
set.add(new Car("奥迪",100));
set.add(new Car("宝马",110));
set.add(new Car("法拉利",100));
set.add(new Car("奥迪",100));
System.out.println("set = " + set);
}
}
class Car{
private String name;
private int price;
public Car(String name, int price) {
this.name = name;
this.price = price;
}
public Car() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return price == car.price &&
Objects.equals(name, car.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
五、Map接口和常用方法
特点介绍:
注意:这里讲的是JDK8的Map接口特点
1、Map与Collection并列存在。用于存放具有映射关系的数据:Key-Value
2、Map中的key和value可以是任何引用类型的数据,会封装HashMap$Node对象中
3、Map中的key不可以重复,原因和HashSet一致(替换机制,会将key相同的旧值替换成新的值)
4、Map中的value可以重复
5、Map的key可以为null,value也可以为null,但是key为null,只能有一个,value为null,可以为多个
6、常用String类作为Map的key,只要是Object的子类都可做key
7、key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
8、Map存放数据的key-value示意图,一对k-v是放在一个Node中的,有因为Node实现了Entry,有的书上也说一对k-v就是一个Entry
//第8点源码
static class Node<K,V> implements Map.Entry<K,V>
import java.util.HashMap;
import java.util.Map;
public class Map_ {
public static void main(String[] args) {
//实现类的特点
Map map = new HashMap();
//1、Map与Collection并列存在。用于存放具有映射关系的数据:Key-Value(双列元素)
//底层是hash,Node结点
//2、Map中的key和value可以是任何引用类型的数据,会封装HashMap$Node对象中
map.put("num1","hello");//k-v
map.put("num2","world");//k-v
//3、Map中的key不可以重复,原因和HashSet一致,当有相同的key,等价于替换
map.put("num2","hhhh");//k-v
//4、Map中的value可以重复
map.put("num3","hhhh");//k-v
//5、Map的key可以为null,value也可以为null,但是key为null,只能有一个,value为null,可以为多个
map.put(null,null);
map.put(null,"abc");//替换map.put(null,null);中的值
map.put("num4",null);
map.put("num5",null);
//6、常用String类作为Map的key,只要是Object的子类都可做key
map.put(1,"张三");
map.put(new Object(),"李四");
//7、key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
System.out.println("key = 1 " + map.get(1));
System.out.println(map.get("key = num5 " + map.get("num5")));
System.out.println("map = " + map);
}
}
代码解读:
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapSource_ {
public static void main(String[] args) {
Map map = new HashMap();
map.put("num1","hello");//k-v
map.put("num2","world");//k-v
//1、k-v最后是存在 HashMap$Node node = newNode(hash, key, value, null); 的对象中
//2、k-y为了方便遍历,还会在底层创建一个EntrySet集合,该集合存放元素的数据类型是Entry,
// 而一个Entry对象就有k、v EntrySet<Entry<K,V>> 即:transient Set<Map.Entry<K,V>> entrySet;
//3、在entrySet中,定义的类型是Map.Entry,但是实际上存放的还是 HashMap$Node,因为HashMap$Node实现了Map.Entry的接口
//即 HashMap$Node implements Map.Entry
//4、把 HashMap$Node对象 存放到entrySet就可方便遍历
//因为Map.Entry提供了两个重要方法,即 K getKey(); 和 V getValue();
Set set = map.entrySet();
System.out.println(set.getClass());
for (Object object : set) {
//System.out.println(object.getClass());
//为了从HashMap$Node对象中取出k-v
//1、首先进行向下转型
Map.Entry entry = (Map.Entry) object;
// System.out.println("key = " + entry.getKey());
// System.out.println("value = " + entry.getValue());
System.out.println(entry.getKey() + "-" + entry.getValue());
}
//获取map集合中的所有key
Set keySet = map.keySet();
System.out.println("keySet = " + keySet);
//获取map集合中的所有value
Collection values = map.values();
System.out.println("values = " + values);
}
}
为了方便管理,把底层的每个node封装为一个Entry,再把每个Entry放在EntrySet中,关系如下,每个key和value是通过内存地址指向的
1、Map接口常用方法
import java.util.HashMap;
import java.util.Map;
public class MapMethod {
public static void main(String[] args) {
Map map = new HashMap();
map.put("book",new Book("三国",120));
map.put("雷军","小米");
map.put("任正非","华为");
map.put("库克","苹果");
map.put("马斯克","特斯拉");
map.put("孙悟空",null);
map.put(null,"唐僧");
//remove:根据key删除value,根据键删除映射关系
map.remove(null);
//get:根据键获取值,根据key获取value
System.out.println("get = " + map.get("雷军"));
//size:获取元素个数
System.out.println("size = " + map.size());
//isEmpty:判断集合个数是否为0
System.out.println("isEmpty = " + map.isEmpty());
//containsKey:查找键是否存在,返回boolean值
System.out.println("containsKey = " + map.containsKey("任正非"));
//clear:清除
map.clear();
System.out.println("clear = " + map);
System.out.println("map = " + map);
}
}
class Book{
private String name;
private int price;
public Book(String name, int price) {
this.name = name;
this.price = price;
}
public Book() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
2、Map的遍历方法
import java.util.*;
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put("雷军","小米");
map.put("任正非","华为");
map.put("库克","苹果");
map.put("马斯克","特斯拉");
map.put("孙悟空","猪八戒");
map.put("白龙马","唐僧");
//第一组:先取出所有的key,通过key取出所有的value
Set keySet = map.keySet();
System.out.println("=================第一组遍历方式=====================");
//(1)增强for循环
System.out.println("-----------第一种:增强for循环-------------");
for (Object key : keySet) {
System.out.println(key + "-" + map.get(key));
}
//(2)迭代器
System.out.println("-----------第一种:迭代器-------------");
Iterator iterator = keySet.iterator();
while (iterator.hasNext()){
//这个iterator.next()就是一个个的key
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
//第二组:把所有的value取出,value是一种集合,
System.out.println("=================第二组遍历方式=====================");
Collection values = map.values();
//(1)增强for循环
System.out.println("-----------第一种:增强for-------------");
for (Object value : values) {
System.out.println("value = " + value);
}
//(2)迭代器
System.out.println("-----------第二种:迭代器-------------");
Iterator valueIterator = values.iterator();
while (valueIterator.hasNext()){
Object value = valueIterator.next();
System.out.println("value = " + value);
}
//第三组:通过EntrySet来获取key-value
System.out.println("=================第三组遍历方式=====================");
Set entrySet = map.entrySet(); //类型 EntrySet<Entry<k,v>>
//(1)增强for
System.out.println("-----------第一种:增强for-------------");
for (Object entry : entrySet) {
//将 entry 转为 Map.Entry
Map.Entry m = (Map.Entry) entry;
//在进行取值
System.out.println(m.getKey() + "-" + m.getValue());
}
//(2)迭代器
System.out.println("-----------第二种:迭代器-------------");
Iterator entryIterator = entrySet.iterator();
while (entryIterator.hasNext()) {
//这里的next的类型是HashMap$Node,但是Node继承了 Map.Entry
Object next = entryIterator.next();
//所以这里就可以将next转为 Map.Entry
Map.Entry m = (Map.Entry) next;
System.out.println(m.getKey() + "-" + m.getValue());
}
}
}
练习:
要求:
使用HashMap添加3个员工对象
键:员工id
值:员工对象
并且遍历显示工资大于18000的员工(遍历方式不少于两种)
员工类:姓名,工资,员工id
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class MapExercise {
public static void main(String[] args) {
Map map = new HashMap();
map.put(1001,new Employee("张三",15000,1001));
map.put(1002,new Employee("李四",19000,1002));
map.put(1003,new Employee("王五",20000,1003));
map.put(1004,new Employee("赵六",11000,1004));
map.put(1005,new Employee("孙七",18000,1005));
map.put(1006,new Employee("张八",14000,1006));
//System.out.println("map = " + map);
//获取所有的key
Set keySet = map.keySet();
//遍历方式一
//(1)增强for循环
System.out.println("-----------第一种遍历:增强for-------------");
for (Object key : keySet) {
//根据key获取值,也就是Employee对象
Object object = map.get(key);
//向下转型
Employee employee = (Employee) object;
//判断薪资
if (employee.getSal() > 18000){
System.out.println(employee);
}
}
//(2)迭代器
System.out.println("-----------第二种遍历:迭代器-------------");
Iterator iterator = keySet.iterator();
while (iterator.hasNext()){
//所有的key
Object key = iterator.next();
//根据key获取对应的Employee对象
Object object = map.get(key);
//向下转型
Employee employee = (Employee) object;
//判断薪资
if (employee.getSal() > 18000){
System.out.println(employee);
}
}
//(3)使用EntrySet
System.out.println("-----------第三种遍历:entrySet-------------");
Set entrySet = map.entrySet();
for (Object entry : entrySet) {
//将 entry 转为 Map.Entry
Map.Entry m = (Map.Entry) entry;
//取出Employee对象,并且向下转型,因为Object对象
Employee employee = (Employee) m.getValue();
//判断薪资
if (employee.getSal() > 18000){
System.out.println(employee);
}
}
}
}
class Employee{
private String name;
private int sal;
private int id;
public Employee(String name, int sal, int id) {
this.name = name;
this.sal = sal;
this.id = id;
}
public Employee() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSal() {
return sal;
}
public void setSal(int sal) {
this.sal = sal;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", sal=" + sal +
", id=" + id +
'}';
}
}
3、HashMap小结
1、Map接口的常用实现类:HashMap、Hashtable和Properties
2、HashMap是Map接口使用频率最高的实现类
3、HashMap是以key-value的方式来存储数据(数据类型是HashMap$Node类型)
4、key不能重复,但是值可以重复,允许使用null键和null值
5、如果添加相同的key,则会覆盖原来的key-value,等同于修改(key不会替换,value会替换)
6、与HashSet一样,不保存映射的顺序,因为底层是以hash表方式来存储的(HashMap底层 数组 + 链表 + 红黑树)
7、HashMap没有实现同步,所以线程不安全
4、HashMap的底层机制及源码剖析
示意图:
table表是个数组,数组中放有链表或者是一棵树,每个节点就是一个Node,Node这个类型又实现了Map.Entry接口
1、(k1,v1)是一个Node实现了Map.Entry<K,V>,查看HashMap的源码可以看到
2、jdk7.0的HashMap底层实现 [数组 + 链表],jdk8.0底层 [数组 + 链表 + 红黑树]
扩容机制:
1、HashMap底层维护了Node类型的数组table,默认为null
2、当创建爱你对象时,将加载因子(loadfactor)初始化为0.75
3、当添加 key-value 时,通过key的哈希值得到在table中的索引,然后判断该索引是否有元素,如果没有元素,直接添加,如果该索引有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,直接替换value;如果不相等需要判断是树结构还是链表结构,做出相应的处理,如果添加时发现容量不够,则需要扩容
4、第1次添加,则需要扩容table容量为16,临界值(threshold)为12
5、以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍,即24,以此类推
6、在java8中,如果一条链表的元素超过TREEIFY_THRESHOLD(默认为8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进化为红黑树
源码剖析:
测试代码:
import java.util.HashMap;
public class MapSource {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("java",10);
map.put("mysql",10);
map.put("java",11);
System.out.println("map = " + map);
}
}
第一步:执行构造器,初始化加载因子,table数组类型是HashMap$Node类型
HashMap$Node[] table = null;
//源码:初始化加载因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//加载因子的值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
第二步:执行put方法
//源码:hash(key)计算key的hash值
public V put(K key, V value) {
//执行putVal方法
//此时的key就是传进来的key,value是传进来的value
return putVal(hash(key), key, value, false, true);
}
//执行的hash()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
第三步:执行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; //辅助变量
//第一步先判断table是否为null,第一次添加时table一定是为null的
if ((tab = table) == null || (n = tab.length) == 0)
//table为null时,首先进行扩容处理,resize()扩容方法
n = (tab = resize()).length;//扩容完毕后,返回一个空的table表,有16个空间
//计算得到了hash值,将这个k-y放到hash值对应的table表索引中,同时将这个值赋值给 p 变量,但是要确保这个索引位置没有元素
if ((p = tab[i = (n - 1) & hash]) == null)
//在table[i]的位置插入该元素,i是根据hash值得到的索引位置,也就是这个元素在table表中的索引位置
//元素的类型是Node
tab[i] = newNode(hash, key, value, null);
else { //当key重复时,就是进入这里
Node<K,V> e; K k; //临时变量
//三种情况
//(1)p的hash值是否与hash值是否相同,这个hash是现在加入的元素的hash值,p.hash是已经存在table中的元素的hash值
//(2)新加入的key是否与p的key是否是同一个对象
//(3)新的key不为空但是内容相同
//这三种情况出现都不在进行添加
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //把p赋值给e,辅助变量
else if (p instanceof TreeNode) //如果这个p是树
//按照红黑树方式添加
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);
//判断链表是否达到了8个元素,如果达到了进行树化,TREEIFY_THRESHOLD = 8
if (binCount >= TREEIFY_THRESHOLD - 1) // 当binCount = 8 时,也就是添加到第9个元素时,开始调用树化方法,但是并不是马上树化(转成红黑树)
treeifyBin(tab, hash); //树化
break;//查询到了链表中有一样的元素,跳出循环,不进行添加
}
//与上面的判断一致
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//不在执行添加,直接跳出循环
p = e; //将e赋值给p
}
}
//key重复,e不为null
if (e != null) { // existing mapping for key
V oldValue = e.value; //将e的值赋值给旧值oldValue
//判断如果这个旧值oldValue为空或者onlyIfAbsent取反满足条件
if (!onlyIfAbsent || oldValue == null)
//将e的value值替换为新的value
e.value = value;
afterNodeAccess(e);
return oldValue;//返回旧的value
}
}
++modCount;
//++size是添加元素后的size,再与临界值比较,如果达到了临界值,开始扩容
if (++size > threshold)
resize(); //扩容
afterNodeInsertion(evict);
return null;
}
第四步:扩容方法 resize()
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //声明一个旧的table,将空的table赋值给oldTab
int oldCap = (oldTab == null) ? 0 : oldTab.length; //判断旧的table容量
int oldThr = threshold; //临界值赋值给旧的临界值
int newCap, newThr = 0; //声明新的容量和新的临界值,赋值为0
if (oldCap > 0) { //判断旧的table容量是否大于0
if (oldCap >= MAXIMUM_CAPACITY) { //如果旧的table容量大于额定的容量
threshold = Integer.MAX_VALUE; //给临界值重新赋值
return oldTab; //返回旧的table的容量
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0) //判断旧的临界值是否大于0
newCap = oldThr;
else {//当上面条件都满足,则就是给默认的table表额定容容量
newCap = DEFAULT_INITIAL_CAPACITY; //指定容量 16
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"})
//这里则生成已经扩容好的table,容量大小由newCap指定,而table表的类型是Node类型
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
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;
}
}
}
}
}
return newTab;
}
5、Hashtable的扩容机制与源码分析
基本介绍:
1、存放的元素时键值对:k-v
2、Hashtable的键和值不能为null,否则抛出NullPointerException异常
3、Hashtable使用方法基本和HashMap一样
4、Hashtable是线程安全的,HashMap是线程不安全的
5、Hashtable的底层是一个Entry数组
演示代码:
import java.util.Hashtable;
import java.util.Map;
public class HashTableMethod {
public static void main(String[] args) {
Hashtable table = new Hashtable();
table.put("java",100); //ok
table.put(null,100); //异常
table.put("mysql",null); //异常
table.put("redis",20); //ok
table.put("redis",100); //替换上面键相同的元素
System.out.println("table = " + table);
}
}
分析底层:
1、底层有数组 Hashtable$Entry[] ,Entry是Hashtable的内部类,继承了Map.Entry,初始大小为11,临界值为8
//源码,初始化调用无参构造,11为初始大小,0.75为加载因子
public Hashtable() {
this(11, 0.75f);
}
2、执行put方法
public synchronized V put(K key, V value) {
// 这里规定的是value不能为null
if (value == null) {
throw new NullPointerException();
}
// 创建一个Entry类型的数组table
Entry<?,?> tab[] = table;
//计算key的hash
int hash = key.hashCode();
//计算元素应该存放的下标位置
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//向下转型,取出table在index位置上的Entry元素
Entry<K,V> entry = (Entry<K,V>)tab[index];
//for循环中判断,如果entry不为null,那就需要进行比较,这个entry上的元素是否与现在插入的元素的key一致
for(; entry != null ; entry = entry.next) {
//判断该位置上的key是否相同
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
//进行替换
entry.value = value;
return old;
}
}
//执行真正的添加方法
addEntry(hash, key, value, index);
return null;
}
3、执行真正的添加方法 addEntry
private void addEntry(int hash, K key, V value, int index) {
modCount++; //记录集合修改次数
Entry<?,?> tab[] = table; //创建一个空的table
//判断table表中的总数是否大于或者等于临界值
if (count >= threshold) {
// 如果超过,则进行扩容
rehash();
tab = table;
hash = key.hashCode(); //获取key的hash值
index = (hash & 0x7FFFFFFF) % tab.length; //计算元素在table表中的下标index
}
// Creates the new entry.
@SuppressWarnings("unchecked")
//找到之前元素在table表中的位置
Entry<K,V> e = (Entry<K,V>) tab[index];
//在table表中index位置存放新的Entry类型元素
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
4、扩容rehash()方法
扩容计算机制:oldCapacity * 2 + 1
扩容后临界值计算机制:(int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1) 新的容量 * 加载因子后,与最大容量 + 1 进行取最小值,取扩容后的临界值
protected void rehash() {
//获取当前table的长度
int oldCapacity = table.length;
//声明一个Entry类型数组的table
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//新的容量计算,旧的容量向左移一位,相当于*2,再加上1
int newCapacity = (oldCapacity << 1) + 1;
//判断新的容量如果大于了额定的最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//再次判断旧的容量是否等于额定的最大容量
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
//以最大容量为标准
newCapacity = MAX_ARRAY_SIZE;
}
//按照最新的容量创建一个Entry类型的数组,大小为已经扩容后的大小
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//重新计算临界值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//将这个新的数组赋值给table表
table = newMap;
//将旧的table表中的元素移动到新的数组上
for (int i = ol dCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
6、Properties的基本介绍
1、Properties类继承了Hashtable类并且实现了Map接口,也是一种使用键值对类型存储数据
2、使用特点与Hashtable类似
3、Properties还可以用于从 xxx.properties 文件中,加载数据到Properties类对象,进行读取和修改
4、xxx.properties 文件通常作为配置文件
基本使用:
import java.util.Properties;
public class Properties_ {
public static void main(String[] args) {
Properties properties = new Properties();
//添加,k-y
properties.put("java",100);
properties.put("mysql",100);
properties.put("python",100);
properties.put("php",100);
//相同的key,value也会被替换,相当于修改
properties.put("php",300);
//因为 properties 是 Hashtable 的子类,所以key和value都不能为null
//properties.put(null,100); //异常
//properties.put("html",null);//异常
//通过key获取value
System.out.println("get = " + properties.get("java"));
//通过key移除元素
properties.remove("mysql");
System.out.println("remove = " + properties);
System.out.println("properties = " + properties);
}
}
六、总结
在开发中,选择什么样集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
1、先判断存储类型(一组对象或一组键值对)
2、一组对象 [单列集合]:Collection接口
-
允许重复:List接口实现类
增删操作多:LinkedList [底层维护了一个双向链表]
改查操作多:ArrayList [底层维护 Object 类型的可变数组]
-
不允许重复:Set接口实现类
无序:HashSet [底层是HashMap,维护了一个哈希表,即(数组 + 链表 + 红黑树)]
排序:TreeSet
插入和取出顺序一致:LinkedHashSet,维护数组 + 双向链表
3、一组键值对 [双列集合]:Map接口实现类
键无序:HashMap [底层是:哈希表 jdk7:数组 + 链表,jdk8:数组 + 链表 + 红黑树 ]
键排序:TreeMap
键插入和取出顺序一致:LinkedHashMap
读取文件:Properties
七、TreeSet和TreeMap的源码剖析
1、TreeSet
特点:
1、相比于HashSet来说,可排序
2、使用无参构造器,创建TreeSet时,使用的是自然排序
3、底层使用的是TreeMap,是Entry类型
4、源码区别
//TreeSet的add方法源码
public boolean add(E e) {
//e是存放进来的值,PRESENT是一个空的Object对象
//private static final Object PRESENT = new Object();
return m.put(e, PRESENT)==null;
}
默认排序:
import java.util.TreeSet;
public class TreeSet_ {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add("java");
treeSet.add("mysql");
treeSet.add("php");
treeSet.add("python");
System.out.println("treeSet = " + treeSet);
}
}
实现接口的匿名内部类进行排序:
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSet_ {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//调用的是String的compareTo方法进行比较
//原理是取出每个字符的长度,进行ASCII码比较
return ((String)o1).compareTo((String)o2);
}
});
treeSet.add("mysql");
treeSet.add("php");
treeSet.add("python");
treeSet.add("java");
System.out.println("treeSet = " + treeSet);
}
}
源码:
第一步:构造器把传入的比较对象(Comparator),赋给了 TreeSet 底层的 TreeMap 属性
//源码
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
第二步:添加元素,添加第一个元素时,不会走比较器
public V put(K key, V value) {
Entry<K,V> t = root; //取出元素,树化
if (t == null) { //只有元素为null时走这里,就是第一次添加时走这里
//传了两个相同的key,检查key是否为空
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
//辅助变量
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
//比较器,声明TreeSet时,传入的比较器,在这里赋值
Comparator<? super K> cpr = comparator;
//当这个比较器不为null时,进行比较
//cpr就是一个匿名内部类
if (cpr != null) {
do {
parent = t;
//动态绑定到匿名内部类(对象)compare方法
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value); //如果相等,cmp为0,数据就加入不了,相当于替换
} while (t != null);
}
//这里是没有匿名内部类的逻辑
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
//根据compareTo进行比较
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;
}
2、TreeMap
特点:
1、传入的是k-y,比较key
默认排序:
在使用无参构造器时,这里的排序是根据ASCII码进行比较的,字母表顺序排序
import java.util.TreeMap;
public class TreeMap_ {
public static void main(String[] args) {
TreeMap treeMap = new TreeMap();
treeMap.put("java",100);
treeMap.put("mysql",200);
treeMap.put("php",300);
treeMap.put("js",400);
System.out.println("treeMap = " + treeMap);
}
}
//输出打印 treeMap = {java=100, js=400, mysql=200, php=300}
使用匿名内部类进行排序:
import java.util.Comparator;
import java.util.TreeMap;
public class TreeMap_ {
public static void main(String[] args) {
//传入匿名内部类
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//比较规则:按照传入的k(String)的大小进行排序
return ((String)o2).compareTo((String)o1);
}
});
treeMap.put("java",100);
treeMap.put("mysql",200);
treeMap.put("php",300);
treeMap.put("js",400);
System.out.println("treeMap = " + treeMap);
}
}
//打印输出 treeMap = {php=300, mysql=200, js=400, java=100}
源码:
第一步:接收传入的实现了匿名内部类(对象)Comparator,传给了TreeMap的comparator属性
//源码
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
第二步:调用add方法
public V put(K key, V value) {
Entry<K,V> t = root;
//第一次添加走这里
if (t == null) {
//传了两个相同的key,检查key是否为空
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
//接收传入的匿名内部类(对象)比较方法
Comparator<? super K> cpr = comparator;
//进入比较器
if (cpr != null) {
//遍历,如果遍历完后没有一个数据与传进来的数据相同,那就会挂到root上
//如果有一个数据相同,那就不会挂载
do {
parent = t;
//这里的值相等判断与HashMap有区别
//HashMap判断值是否相等是通过hash或者equals方法等进行判断的
//这里的相等是通过compare方法决定的
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value); //有相同的,替换
} while (t != null);
}
//走默认的比较规则
else {
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;
}
八、Collections工具类
基本介绍:
1、Collections是一个操作Set、List和Map等集合的工具类
2、Collections中提供了一系列静态方法对集合元素进行排序、查询和修改等操作
排序操作:(均为static方法)
1、reverse(List):反转 List 中元素顺序
2、shuffle(List):对 List 集合元素进行随机排序
3、sort(List):根据元素的自然顺序对指定List集合元素按升序排序
4、sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
5、swap(List,int,int):将指定List集合中的 i 处元素和 j 处元素进行交换
测试代码:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Collections_ {
public static void main(String[] args) {
List list = new ArrayList();
list.add("java");
list.add("mysql");
list.add("css");
list.add("html");
list.add("js");
//reverse(List):反转 List 中元素顺序
Collections.reverse(list);
System.out.println("reverse = " + list);
//shuffle(List):对 List 集合元素进行随机排序
Collections.shuffle(list);
System.out.println("shuffle = " + list);
//sort(List):根据元素的自然顺序对指定List集合元素按升序排序
Collections.sort(list);
System.out.println("sort = " + list);
//sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
Collections.sort(list, new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
//自定义排序规则
return ((String)o1).length() - ((String)o2).length();
}
});
System.out.println("sort自定义排序 = " + list);
//swap(List,int,int):将指定List集合中的 i 处元素和 j 处元素进行交换
Collections.swap(list, 2, 3);
System.out.println("swap = " + list);
}
}
查找、替换:
1、Object max(Collection):根据元素的自然顺序,返回给定集合中最大元素
2、Object max(Collection,Comparator):根据Comparator指定的顺序,返回集合中最大的元素
3、Object mix(Collection):根据元素的自然顺序,返回给定集合中最小元素
4、Object mix(Collection,Comparator):根据Comparator指定的顺序,返回集合中最小的元素
5、int frequency(Collection,Object):返回指定集合中指定元素出现次数
6、void copy(List dest,List src):将src的内容复制到dest中
7、boolean replayceAll(List list,Object oldVal,Object newVal):使用新的值替换List对象的所有旧值
演示代码:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Collections_ {
public static void main(String[] args) {
List list = new ArrayList();
list.add("java");
list.add("mysql");
list.add("css");
list.add("html");
list.add("js");
//Object max(Collection):根据元素的自然顺序,返回给定集合中最大元素
System.out.println("自然排序最大值:" + Collections.max(list));
//Object max(Collection,Comparator):根据Comparator指定的顺序,返回集合中最大的元素
System.out.println("自定义规则最大值:" + Collections.max(list, new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
//自定义比较规则
return ((String)o1).length() - ((String)o2).length();
}
}));
//参考max即可
//Object mix(Collection):根据元素的自然顺序,返回给定集合中最小元素
//Object mix(Collection,Comparator):根据Comparator指定的顺序,返回集合中最小的元素
//int frequency(Collection,Object):返回指定集合中指定元素出现次数
System.out.println("frequency:" + Collections.frequency(list,"java"));
//void copy(List dest,List src):将src的内容复制到dest中
ArrayList dest = new ArrayList();
//拷贝前需要给dest赋值,长度与list现在的长度一致
for (int i = 0; i < list.size(); i++) {
dest.add("");
}
//拷贝
Collections.copy(dest,list);
System.out.println("dest = " + dest);
//boolean replayceAll(List list,Object oldVal,Object newVal):使用新的值替换List对象的所有旧值
Collections.replaceAll(list,"java","javaee");
System.out.println("replaceAll = " + list);
}
}