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进行演示

copy
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本身不存放对象

迭代器原理

迭代器代码

copy
//得到一个集合的迭代器 Iterator iterator = list.iterator(); //判断集合是否还有元素,没有返回false,退出循环 while (iterator.hasNext()){ //打印元素 //next() 1、指针下移 2、将下移以后集合位置上的元素返回 System.out.println(iterator.next()); }

迭代器原理图

注意:在调用it.next()方法之前必须调用it.hasNext()进行检测,如果不调用,且下一条记录无效时,直接调用it.next()方法会抛出NoSuchElementException异常

copy
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的源码下

copy
//源码,返回一个迭代器 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]; }
copy
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循环练习

copy
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集合类中元素有序(即添加顺序和取出顺序一致),且元素可重复

copy
//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集合中的每个元素都有对应的顺序索引,即支持索引

copy
//2、List集合中的每个元素都有对应的顺序索引,即支持索引 //索引从0开始 System.out.println(list.get(0));

3、List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素

4、JDK API中List接口的常用实现类有 ArrayList、LinkedList和Vector

2、常用方法

copy
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练习

copy
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方式

    copy
    //迭代器 Iterator iterator = list.iterator(); while (iterator.hasNext()){ Object object = iterator.next(); System.out.println("object = " + object); }
  • 方式二:增强for循环

    copy
    //增强for for (Object object : list) { System.out.println("object = " + object); }
  • 方式三:使用普通的for

    copy
    //普通for for (int i = 0; i < list.size(); i++) { System.out.println("object = " + list.get(i)); }

5、练习,冒泡排序

创建Book对象

copy
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; } }

进行测试

copy
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,并且多个

    copy
    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

    copy
    //源码 没有加 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倍

源码分析:

测试代码:

copy
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); } }

无参构造分析源码:

copy
//无参构造 ArrayList list = new ArrayList();

第一步:ArrayList第一次初始化的就是创建一个空数组

copy
/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { //创建空数组 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //空数组定义 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

第二步:执行添加的方法,在添加元素之前,需要进行判断是否要扩容

copy
/** * 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

copy
//该方法用于判断是否扩容 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; }

第四步:记录当前集合被修改的次数,判断最小容量是否足够,不够就执行扩容方法,每次添加元素都会执行该方法判断是否要扩容

copy
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()的保留之前的数据,然后再进行扩容

copy
/** * 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); }

第六步:扩容完成后,返回到添加方法

copy
/** * 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; }

有参构造分析源码:

copy
//有参构造 ArrayList list = new ArrayList(8);

第一步:对传入的参数进行判断,创建指定大小的elementData数组 new Object[initialCapacity]

copy
/** * 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、继承关系

copy
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

2、Vector底层也是对象数组

copy
protected Object[] elementData;

3、Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized

源码例子

copy
//源码带有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倍扩容

源码解读:

copy
//创建集合 Vector vector = new Vector(); for (int i = 0; i < 10; i++) { vector.add(i); }

第一步:构建无参构造器,实际上是执行了有参构造器,默认是10

copy
//源码,默认给10的容量,实际上调用的是有参构造器,默认长度是10 public Vector() { this(10); } public Vector(int initialCapacity) { this(initialCapacity, 0); }

第二步:添加数据

copy
public synchronized boolean add(E e) { modCount++;//记录集合的修改次数 ensureCapacityHelper(elementCount + 1); //判断扩容 elementData[elementCount++] = e;//给集合赋值 return true; }

第三步:判断是否需要真的扩容

copy
private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code //判断是否需要真正的扩容 if (minCapacity - elementData.length > 0) grow(minCapacity);//扩容处理 }

第四步:执行扩容机制,计算扩容的算法与ArrayList不太一致,Vector使用的是2倍扩容

copy
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、双向链表的模拟实现

copy
/** * 模拟一个简单的双向链表 */ 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的源码图解

添加节点的源码:

第一步:

copy
//创建LinkedList LinkedList linkedList = new LinkedList(); //执行添加 linkedList.add(1);

这时会调用一个无参构造的方法,初始的属性 first = null,last = null,size = 0

copy
//首先会调用无参构造方法(源码) public LinkedList() {}

第二步:执行add方法

copy
//源码 public boolean add(E e) { linkLast(e); return true; }

linkLast方法的实现,当只有一个元素时,first 和 last 都指向这个节点,元素都是从尾进行插入的,因此刚刚插入的数据都是赋值给last

当第一次添加数据,last = null,就会把新的元素赋值给 last ,根据 if (l == null) ,也将第一个元素赋值给 first

第二次插入元素,last不为空,因为之前已经有一个数据了,所以这个 l.next 指向下一个元素

copy
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 }

删除第一个节点的源码:

copy
//默认删除的是第一个节点 linkedList.remove();

第一步:调用删除第一个元素的方法

copy
//源码(执行了删除第一个的方法) public E remove() { return removeFirst(); }

第二步:判断第一个元素的first是否为空 first == null

copy
//源码 public E removeFirst() { final Node<E> f = first; //将第一个元素的first赋值给 f if (f == null) //如果f为空,直接抛出异常 throw new NoSuchElementException(); return unlinkFirst(f); //不为空调用unlinkFirst方法,传入f }

第三步:执行真正的删除方法

copy
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、不能使用索引的方式来获取

基本演示

copy
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

copy
//构造器 Set set = new HashSet(); //源码 public HashSet() { map = new HashMap<>(); }

3、可以存放null值,但是只能有一个null

4、HashSet不保证元素时有序的,取决于hash后,确定索引的结果(不保证存放元素的顺序和取出的顺序一致)

5、不能存放重复元素/对象

演示

copy
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底层是(数组+链表+红黑树)

copy
/** * 需要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

copy
HashSet set = new HashSet(); //源码 public HashSet() { map = new HashMap<>(); }

第一次,添加add源码

第二步:添加数据,执行add

copy
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冲突的几率,执行了一个算法

copy
//源码 //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方法

copy
//源码 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方法使用的不一致

copy
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),就会进行树化(红黑树),否则仍然采用数组扩容机制

演示代码:

copy
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 中的所有元素?

测试:

copy
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集合中

copy
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集合中

copy
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也能确保插入顺序和遍历顺序一致

分析底层:

检测代码:

copy
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()源码:

copy
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方法

copy
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

copy
//第8点源码 static class Node<K,V> implements Map.Entry<K,V>
copy
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); } }

代码解读:

copy
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接口常用方法

copy
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的遍历方法

copy
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

copy
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),就会进化为红黑树

源码剖析:

测试代码:

copy
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;

copy
//源码:初始化加载因子 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //加载因子的值 static final float DEFAULT_LOAD_FACTOR = 0.75f;

第二步:执行put方法

copy
//源码: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

copy
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()

copy
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数组

演示代码:

copy
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

copy
//源码,初始化调用无参构造,11为初始大小,0.75为加载因子 public Hashtable() { this(11, 0.75f); }

2、执行put方法

copy
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

copy
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 进行取最小值,取扩容后的临界值

copy
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 文件通常作为配置文件

基本使用:

copy
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、源码区别

copy
//TreeSet的add方法源码 public boolean add(E e) { //e是存放进来的值,PRESENT是一个空的Object对象 //private static final Object PRESENT = new Object(); return m.put(e, PRESENT)==null; }

默认排序:

copy
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); } }

实现接口的匿名内部类进行排序:

copy
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 属性

copy
//源码 public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); }

第二步:添加元素,添加第一个元素时,不会走比较器

copy
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码进行比较的,字母表顺序排序

copy
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}

使用匿名内部类进行排序:

copy
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属性

copy
//源码 public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }

第二步:调用add方法

copy
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 处元素进行交换

测试代码:

copy
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对象的所有旧值

演示代码:

copy
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); } }
posted @   花椒蛋炒饭  阅读(64)  评论(0编辑  收藏  举报
相关博文:
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起