Java集合详解且易懂

本篇Java集合内容是根据b站韩顺平教学视频总结归纳得出的,不涉及泛型,包含一些源码的解读

目录

集合框架体系图

​编辑

集合和数组的区别

常用的集合分类

Collection接口

Collection接口常用方法

Collection的两种遍历方式

 List和Set详解

List和Set的区别

 List接口和常用方法

List的三种遍历方式

ArrayList底层结构和源码分析

Vector底层结构和源码分析 

LinkedList底层

Set接口介绍

Set接口实现类HashSet

LinkedHashSet

Map接口介绍

Map接口常用方法

Map遍历方式

HashMap小结

HashMap底层机制和源码分析

HashTable介绍

 TreeSet

TreeMap 

Map 接口实现类-Properties 

 总结:开发中如何选择集合类


集合框架体系图

 如图所示:图中,实线边框的是实现类,虚线边框的是抽象类,而点线边框的是接口

Collection接口是集合类的总接口,它没有直接实现的类,但却有两个继承的接口set和list

set中不能有重复的元素,list可以包含重复元素是一个有序的集合,提供了按索引访问的方式

Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value。

Iterator:所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含以下三种方法:

1.hasNext()是否还有下一个元素。

2.next()返回下一个元素。

3.remove()删除当前元素。

后序也有通过Iterator来遍历集合元素的方式

集合和数组的区别

 

常用的集合分类

Collection接口

Collection接口常用方法

1.add() 添加元素
2.size() 获取集合中元素的个数
3.addAll() 把一个集合的元素添加到一个新的集合
4.isEmpty() 判断当前集合是否为空,判断的是集合中是否有元素
5.contains() 判断是否包含
6.containsAll() 判断集合coll1中是否包含coll集合中的所有元素
7.remove() 删除指定元素
8.removeAll() 删除当前集合中包含另一个集合中的所有元素
9.clear() 清空集合中的元素

public class Collection02 {
    public static void main(String[] args) {
        Collection col = new ArrayList();

        //add() 添加元素
        col.add("AA");
        col.add(123);
        col.add(new String("A"));

        //size() 获取集合中元素的个数
        System.out.println(col.size());

        //addAll()  把一个集合的元素添加到一个新的集合
        Collection coll = new ArrayList();
        coll.add(456);
        coll.addAll(col);


        //isEmpty() 判断当前集合是否为空,判断的是集合中是否有元素
        System.out.println(col.isEmpty());//返回true表示没有元素

        //contains() 判断是否包含
        System.out.println(col.contains(123));

        //containsAll()   判断集合coll1中是否包含coll集合中的所有元素
        System.out.println(coll.containsAll(coll));

        //remove()  删除指定元素
        coll.remove(123);  //删除成功返回true,没找到返回false
        System.out.println(coll);

        //removeAll()   删除当前集合中包含另一个集合中的所有元素
        coll.removeAll(col);
        System.out.println(coll);

        //clear()   清空集合中的元素
        col.clear();

    }
}

运行结果

3
false
true
true
[456, AA, A]
[456]
[]

 10.retainAll() 获取两个集合的交集

11.equels() 比较两个集合是否相等

12.hashCode() 返回当前对象的哈希值

13.toArray() 集合转成数组

14.数组转成集合 Arrays.asList()

public class Collection02 {
    public static void main(String[] args) {
        Collection col = new ArrayList();


        col.add("AA");
        col.add(123);
        col.add(new String("A"));


        System.out.println(col.size());

        Collection coll = new ArrayList();
        coll.add(456);
        coll.addAll(col);
        System.out.println(col);
        System.out.println(coll);

        //retainAll()   获取两个集合的交集
        coll.retainAll(col);
        System.out.println(coll);

        //equels()  比较两个集合是否相等
        System.out.println(coll.equals(col));

        //hashCode()    返回当前对象的哈希值
        System.out.println(coll.hashCode());

        //toArray() 集合转成数组
        Object[] o = coll.toArray();
        System.out.println(Arrays.toString(o));


        //数组转成集合    Arrays.asList()
        System.out.println(Arrays.asList(o));

    }
}

运行结果

3
[AA, 123, A]
[456, AA, 123, A]
[AA, 123, A]
true
2032549
[AA, 123, A]
[AA, 123, A]

Collection的两种遍历方式

public class Collection02 {
    public static void main(String[] args) {
        Collection col = new ArrayList();


        col.add("AA");
        col.add(123);
        col.add(new String("A"));


        System.out.println(col.size());
        //第一种迭代器的方式
        Iterator iterator = col.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println( next);
        }
        System.out.println("=============");
        //第二种增强for循环
        for (Object obj:col) {
            System.out.println(obj);
        }

注意:

当退出 while 循环后 , 这时 iterator 迭代器,指向最后的元素
iterator.next();//这里会造成空指针异常
如果希望再次遍历,需要重置我们的迭代器
 iterator = col.iterator(); 

迭代器运行图解 

 List和Set详解

List和Set的区别

 List接口和常用方法

代码演示:

public class ListMethod {

    public static void main(String[] args) {

        List list = new ArrayList();//接口的多态
        list.add("张三丰");
        list.add("贾宝玉");
        //void add(int index, Object ele);//在 index 位置插入 ele
        //在 index = 1 的位置插入一个对象 原先位置上的对象往后平移一个位置
        list.add(1,"韩顺平");
        System.out.println("list=" + list);

        // boolean addAll(int index, Collection eles):
        // 从 index 位置开始将 eles 中的所有元素添加进来  加多个元素
        List list2 = new ArrayList();
        list2.add("jack");
        list2.add("tom");
        list.addAll(1, list2);
        System.out.println("list=" + list);

        // Object get(int index):获取指定 index 位置的元素
        System.out.println(list.get(0));

        // int indexOf(Object obj):返回 obj 在集合中首次出现的位置
        System.out.println(list.indexOf("tom"));//2

        // int lastIndexOf(Object obj):返回 obj 在当前集合从右往左查找的第一个对象
        list.add("韩顺平");
        System.out.println("list=" + list);
        System.out.println(list.lastIndexOf("韩顺平"));

        // Object remove(int index):移除指定 index 位置的元素,并返回此元素
        System.out.println(list.remove(0));//输出移除的元素
        System.out.println("list=" + list);

        // Object set(int index, Object ele):
        // 设置指定 index 位置的元素为 ele , 相当于是替换.
        list.set(1, "jack");
        System.out.println("list=" + list);

        // List subList(int fromIndex, int toIndex):
        // 返回从 fromIndex 到 toIndex 位置的子集合
        // 注意返回的子集合 fromIndex <= subList < toIndex
        // 这里(0,2)中不包括2  包括0 和 1
        List returnlist = list.subList(0, 2);
        System.out.println("returnlist=" + returnlist);

    }
}

List的三种遍历方式

public class ListFor {

    public static void main(String[] args) {

        /*
        三种遍历list的方式
        1.迭代器
        2.增强for循环
        3.普通for循环
         */
		
        //Vector LinkedList ArrayList都是实现List接口的子类
        List list = new ArrayList();
        //List list = new Vector();
        //List list = new LinkedList();
        list.add("jack");
        list.add("tom");
        list.add("鱼香肉丝");
        list.add("北京烤鸭子");
        
        //遍历
        //1. 迭代器
        Iterator iterator = list.iterator();
        while(iterator.hasNext()){//快捷键是itit
            Object next = iterator.next();
            System.out.println(next);
        }
        System.out.println("==============");
        //2.增强for循环
        for (Object obj:list) {
            System.out.println(obj);
        }
        //3.普通for循环
        System.out.println("=============");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));//通过get索引的方式获取每个元素
        }
    }
}

ArrayList底层结构和源码分析

1.ArrayList支持加入null 并且多个
2.ArrayList 是由数组来存储的
3.ArrayList 基本等同于Vector 除了ArrayList是线程不安全的 多线程不建议使用 

初始化方式容量数量变化
List arrayList = new ArrayList();初始数组容量为10,当真正对数组进行添加时,才真正分配容量10->15->22->33->49->74->…
List arrayList = new ArrayList(8);88->12->->18->27->…

 接下来我们看扩容的源码解读-----这里我们以第一种初始化方式举例

//1.创建一个arraylist对象
ArrayList arrayList = new ArrayList();

 底层原理:调用无参构造器

 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//这是一个空数组  意味着最开始elementData.length =0 
    }
// private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

调用add方法 

 arrayList.add("test");

add方法底层原理

public boolean add(E e) {
        // 确认elementData容量是否足够
        ensureCapacityInternal(size + 1);  // 第一次调用add()方法时,size=0
        elementData[size++] = e;
        return true;
    }

 先调用ensureCapacityInternal(int minCapacity) 方法,对数组容量进行检查,不够时则进行扩容。

private void ensureCapacityInternal(int minCapacity) {
    // 如果elementData为"{}"即第一次调用add(E e),重新定义minCapacity的值,赋值为DEFAULT_CAPACITY=10
    // 即第一次调用add(E e)方法时,定义底层数组elementData的长度为10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
	// 下面这个方法判断是否需要扩容
        ensureExplicitCapacity(minCapacity);
    }

ensureExplicitCapacity(minCapacity) 判断是否需要扩容 

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

// 第一次进入时,minCapacity=10,elementData.length=0,对数组进行扩容 --- 这一步还没有添加元素所以length = 0
    //比如第二次进入时 mincapacity = 2 elementData = 10   
// 之后再进入时,minCapacity=size+1,elementData.length=10(每次扩容后会改变),
    // 因为这一个方法  ensureCapacityInternal(size + 1);  所以mincapacity = size + 1
// 需要minCapacity>elementData.length成立,才能扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

扩容具体方法

private void grow(int minCapacity) {
        // 将数组长度赋值给oldCapacity
        int oldCapacity = elementData.length;
    	// 将oldCapacity右移一位再加上oldCapacity,即相当于newCapacity=1.5oldCapacity(不考虑精度损失)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
    	// 如果newCapacity还是小于minCapacity,直接将minCapacity赋值给newCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
    	// 特殊情况:newCapacity的值过大,直接将整型最大值赋给newCapacity,
	// 即newCapacity=Integer.MAX_VALUE
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 将elementData的数据拷贝到扩容后的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

// 如果大于临界值,进行整型最大值的分配
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

总结: 使用ArrayList()创建ArrayList对象时,不会定义底层数组的长度,当第一次调用add(E e) 方法时,初始化定义底层数组的长度为10,之后调用add(E e)时,如果需要扩容,则调用grow(int minCapacity) 进行扩容,长度为原来的1.5倍 

Vector底层结构和源码分析 

1.Vector底层也是一个对象数组
2.Vector是线程同步,即线程安全,Vector类的操作方法带有synchronized
3.开发中,需要线程同步时使用Vector

Vector扩容机制与Arraylist类似,但它是扩容两倍不是1.5倍,但是它可以指定扩容因子,也就是扩容倍数,而Arraylist不能

1、当使用的是无参构造函数创建Vector对象时,默认会初始化容量为10,每次扩容,容量 = 原容量 × 2

Vector vector = new Vector();

2、当使用的是一个参数的有参构造函数创建Vector对象时,初始化容量则为指定的长度,每次扩容,容量 = 原容量 × 2

Vector vector = new Vector(8);

3、当使用的是两个参数的有参构造函数创建Vector对象时,初始化容量则为指定的长度,每次扩容,容量 = 原容量 + 指定的扩容长度

Vector vector = new Vector(8,2);

LinkedList底层

LinkedList底层是一个双向链表,这与arraylist的存储数据方式就不同了,arraylist是通过动态数组来实现的,下面是图例演示

代码演示使用LinkedList增删改查 

public class LinkedList01 {
    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList();
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
        System.out.println("linkedList=" + linkedList);

        //演示一个删除结点的
        //linkedList.remove(); //  这里默认删除的是第一个结点
        linkedList.remove(2);

        System.out.println("linkedList=" + linkedList);

        //修改某个结点对象
        linkedList.set(1, 999);
        System.out.println("linkedList=" + linkedList);
        //得到某个结点对象
        //get(1) 是得到双向链表的第二个对象
        Object o = linkedList.get(1);
        System.out.println(o);//999

        //因为 LinkedList  是 实现了 List 接口,  遍历方式
        System.out.println("===LinkeList 遍历迭代器====");
        Iterator iterator = linkedList.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println("next=" + next);

        }

        System.out.println("===LinkeList 遍历增强 for====");
        for (Object o1 : linkedList) {
            System.out.println("o1=" + o1);
        }
        System.out.println("===LinkeList 遍历普通 for====");
        for (int i = 0; i < linkedList.size(); i++) {
            System.out.println(linkedList.get(i));
        }

    }
}

Add方法源码解读

void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;//last指向当前加入的元素
        if (l == null)
            first = newNode;//如果l是null说明当前是一个空链表,因此first就指向这个加入的元素
        else
            l.next = newNode;//如果l不是空说明当前链表不为空,那么first节点不用移动,而是把原先last的后面挂载上新数据
        size++;              //因此到这里就能明白为什么前面要有一个l = last的动作
        modCount++;
    }

remove方法源码解读 

private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;//移动first
        if (next == null)
            last = null;//next == null如果成立说明后续没有多的元素了,即删除的是最后一个元素
        else
            next.prev = null;//否则说明后续还有元素,那么next.prev就是当前删除元素的下一个元素的前一个指向置为null
        size--;
        modCount++;
        return element;
    }

 

总结与对比

底层结构增删的效率改查的效率
Arraylist可变数组较低,因为涉及数组扩容较高,索引查找
linkedlist双向链表较高较低

Set接口介绍

1.不能存放重复元素,只能有一个null

2.Set是无序的,添加和取出顺序不一致,没有索引

3.Set的实现类主要有HashSet和TreeSet

4.Set常用方法和Collection一样

5.Set的遍历方式---->迭代器,增强for循环,不能使用索引

//方式一使用迭代器
	System.out.println("=====使用迭代器===="); Iterator iterator = set.iterator();
	while (iterator.hasNext()) { Object obj =	iterator.next();
	System.out.println("obj=" + obj);
}
	//方式 2:  增强 for
	System.out.println("=====增强 for====");
	for (Object o : set) { System.out.println("o=" + o);
}

Set接口实现类HashSet

HashSet实际上是HashMap(源码了解)  

 

源码解读


        1、执行HashSet()
            public HashSet() {
                map = new HashMap<>();
            }
        2、执行 add()
             public boolean add(E e) {
                return map.put(e, PRESENT)==null;//PRESENT =private static final Object PRESENT = new Object();
             }
        3、执行 put(),该方法会执行 hash(key)得到key对应的hash值
            public V put(K key, V value) {//这个key就是我们传入的数据,value是PRESENT ,不会改变,用于占位
                return putVal(hash(key), key, value, false, true);
            }
         4、执行 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语句表示如果当前table 是null,或者 大小=0
        /就是第一次扩容,到16个空间
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //(1)根据key,得到hash去计算该key应该存放到table表的哪个索引位置
        //并且把这个位置的对象赋给 P
        //(2)判断p是否为null
        //(2.1)如果p 为null,表示还没有存放元素,就创建一个Node
        //(2.2)就放在该位置tab[i] = newNode(hash, key, value, null);
        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个结点
            //      如果达到,则调用treeifyBin()对当前链表进行红黑树化
            //      在转化为红黑树的时候要进行判断,判断条件
            //      if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            //            resize();
            //      条件成立先table扩容
            //      当条件不成立的时候才进行转化为红黑树
            //(2)如果有相同的情况,就直接break
                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;
        //在这里真正实现扩容
        //size 是我们每加入一个结点Node(k,v,h,next)
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
         

 总结HashSet的执行机制

1.添加一个元素时,先获得它的hash值,这里的hash值着hashcode值不一样,然后转为索引值

2.找到存储数据表table,看这个索引位置是否有元素

3.如果没有直接加入

4.如果有就调用equals比较,相同就放弃添加,不相同就加在最后面

5.Java8中如果table的一条链表元素超过8个,且table元素的大小超过64就会红黑树化

注意点 -- Tips

set.add(new Dog("tom"));//OK
        set.add(new Dog("tom"));//Ok  这两个是不同对象

		set.add(new String("hsp"));//ok
        set.add(new String("hsp"));//加入不了.
class Dog { //定义了 Dog 类
    private String name;
    public Dog(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

这里我们需要了解为什么同样是new 一个是自定义的Dog类添加成功,另一个String类却没有成功,这里需要看String的源码进行分析

String源码分析

下面是String的hashCode的源码,首先是h被hash赋值了,而hash默认是0,这里的value数组其实就是String的字符数组。两个String对象值一样的话,value数组必然也是一样的。后面没啥好看的了,h是0,value数组完全相等,那么经过if语句里的一通操作,返回的h值还是一样的。  

 

private final char value[];

private int hash; // Default to 0

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

我们可以得出结论:只要两个String对象的值相等,那么他们的hashCode一定是相等的。  

LinkedHashSet

LinkedHashSet底层是维护了一个hash表和双向链表,它是按照hashcode值来决定存储位置,因此让其看起来是取出和加入的顺序一致,其同样不允许添加相同元素

这里当我们添加的元素是自定义的类时,我们可以重写其equals方法来判断符合哪些条件时判断两者为同一对象,下面进行代码演示

 

//这里是重写car的equals方法,当车的名字和价钱一致时认为是同一数据,就不重复添加

public class LinkedHashSetExercise {
    public static void main(String[] args) {

        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add(new Car("奥拓", 1000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//OK
        linkedHashSet.add(new Car("法拉利", 10000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了
        linkedHashSet.add(new Car("保时捷", 70000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了
        System.out.println("linkedHashSet=" + linkedHashSet);
    }
}
class Car {
    private String name;
    private double price;

    public Car(String name, double price) {
        this.name = name;
        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 Double.compare(car.price, price) == 0 &&
                Objects.equals(name, car.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }
}

Map接口介绍

1.Map和Collection并列存在,用于保存具有映射关系的数据,key-value

2.Map中的key不允许重复,原因和hashset一样,前面分析过源码

3.map中的value可以重复

4.map的key可以为null,value也可以为null,注意key为null只能有一个,value为null可以有多个

5.map中的key和value可以是任何引用类型的数据,会封装到hashmap$Node对象中

public class Map01 {

    public static void main(String[] args) {

        // 解读 Map 接口实现类的特点, 使用实现类 HashMap
        //1. Map 与 Collection 并列存在。用于保存具有映射关系的数据:Key-Value(双列元素)
        //2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
        //3. Map 中的 key 不允许重复,原因和 HashSet 一样,前面分析过源码. //4. Map 中的 value 可以重复
        //5. Map 的 key 可以为 null, value 也可以为 null ,注意 key 为 null
        // 只能有一个,value 为 null ,可以多个
        //6. 常用 String 类作为 Map 的 key
        //7. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value
        Map map = new HashMap();
        map.put("no1", "韩顺平");//k-v
        map.put("no2", "张无忌");//k-v
        map.put("no1", "张三丰");//当有相同的 k , 就等价于替换.
        map.put("no3", "张三丰");//k-v
        map.put(null, null); //k-v
        map.put(null, "abc"); //等价替换
        map.put("no4", null); //k-v
        map.put("no5", null); //k-v
        map.put(1, "赵敏");//k-v
        map.put(new Object(), "金毛狮王");//k-v
        map.put(new Object(), "金毛狮王");//true
        // 通过 get 方法,传入 key ,会返回对应的 value
        System.out.println(map.get("no2"));//张无忌
        System.out.println("map=" + map);
    }
}

 

Map接口常用方法

put:添加数据

remove:根据键删除映射关系

get:根据键获取值

size:获取元素个数

isEmpty:判断个数是否为 0

clear:清除 k-v

containsKey:查找键是否存在

 

public class MapMethod01 {
    public static void main(String[] args) {

        //演示 map 接口常用方法
        Map map = new HashMap();
        map.put("邓超", new Book("", 100));//OK
        map.put("邓超", "孙俪");//替换-> 一会分析源码
        map.put("王宝强", "马蓉");//OK
        map.put("宋喆", "马蓉");//OK
        map.put("刘令博", null);//OK
        map.put(null, "刘亦菲");//OK
        map.put(null, null);//OK
        map.put("鹿晗", "关晓彤");//OK
        map.put("hsp", "hsp 的老婆");
        System.out.println("map=" + map);
        // remove:根据键删除映射关系
        map.remove(null);//如果有多个key为null 会一并删除
        System.out.println("map=" + map);
        // get:根据键获取值
        Object val = map.get("鹿晗");
        System.out.println("val=" + val);
        // size:获取元素个数
        System.out.println("k-v=" + map.size());
        // isEmpty:判断个数是否为 0
        System.out.println(map.isEmpty());//F
        // clear:清除 k-v
        map.clear();//全部删除
        System.out.println("map=" + map);
        // containsKey:查找键是否存在
        System.out.println("结果=" + map.containsKey("hsp"));//t

    }
}
class Book {
    private String name;
    private int num;
    public Book(String name, int num) {
        this.name = name;
        this.num = num;
    }
}

Map遍历方式

public class MapFor {

    public static void main(String[] args) {

        Map map = new HashMap();
        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("宋喆", "马蓉");
        map.put("刘令博", null);
        map.put(null, "刘亦菲");
        map.put("鹿晗", "关晓彤");


        //第一组: 先取出 所有的 Key , 通过 Key 取出对应的 Value
        Set keyset = map.keySet(); //是set类型 有增强for循环和迭代器两种方式  这一步将所有的key放到keyset
        //1.增强for循环
        System.out.println("第一种方式");
        for (Object key : keyset) { //将所有的key依次赋给key 再调用map方法 通过key取出value
            System.out.println(key + " - " + map.get(key));
        }
        System.out.println();

        //2.通过迭代器
        System.out.println("第二种方式");
        Iterator iterator = keyset.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();//这个next是k-v 中的key
            System.out.println(next + " - " + map.get(next));
        }
        System.out.println("====================");

        //第二组方式 把所有的values取出
        Collection values = map.values(); //是collection对象 有两种方式遍历
        for (Object obj : values) {
            System.out.println(obj);
        }
        System.out.println();
        //迭代器,同Collection遍历

        //第三组,使用EntrySet来获取 k-v
        Set entrySet = map.entrySet();//类型是EntrySet<Map.Entry<K,V>>
        //1.增强for循环
        System.out.println("使用Entry的增强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("使用EntrySet迭代器遍历");
        Iterator iterator1 = entrySet.iterator();
        while (iterator1.hasNext()) {
            Object next = iterator1.next();
            System.out.println(next.getClass());//HashMap$Node---实现Map.Entry
            //向下转型
            Map.Entry m = (Map.Entry) next;
            System.out.println(m.getKey() + " - " + m.getValue());
        }
    }
}

HashMap小结

 

HashMap底层机制和源码分析

 

 

 代码演示

public class HashMapSource1 {
public static void main(String[] args) { 
    HashMap map = new HashMap(); 
    map.put("java", 10);//ok
	map.put("php", 10);//ok map.put("java", 20);//替换 value

	System.out.println("map=" + map);//


    /*解读 HashMap 的源码+图解
    1.	执行构造器 new HashMap()
    初始化加载因子 loadfactor = 0.75 HashMap$Node[] table = null
    2.	执行 put 调用 hash 方法,计算 key 的 hash 值 (h = key.hashCode()) ^ (h >>> 16) public V put(K key, V value) {//K = "java" value = 10
    return putVal(hash(key), key, value, false, true);
    }
    3.	执行 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,  或者 length =0 ,  就扩容到 16 
    if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
	//取出 hash 值对应的 table 的索引位置的 Node,  如果为 null,  就直接把加入的 k-v
    //, 创建成一个 Node ,加入该位置即可if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null); else {
    Node<K,V> e; K k;//辅助变量
    // 如果 table 的索引位置的 key 的 hash 相同和新的 key 的 hash 值相同,
    // 并 满足(table 现有的结点的 key 和准备添加的 key 是同一个对象	|| equals 返回真)
    // 就认为不能加入新的 k-v if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
    else if (p instanceof TreeNode)//如果当前的 table 的已有的 Node 是红黑树,就按照红黑树的方式处
    理
    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 个,到 8 个,后
    //就调用 treeifyBin 方法进行红黑树的转换
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);
    break;
}
   if (e.hash == hash &&  value ((k = e.key) == key || (key != null && key.equals(k))))
   //如果在循环比较过程中,发现有相同,就 break,就只是替换
    break; 
    p = e;
  }
}
    if (e != null) { // existing mapping for key V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
    e.value = value; //替换,key 对应 value afterNodeAccess(e);
    return oldValue;
    }
}
    ++modCount;//每增加一个 Node ,就 size++
    if (++size > threshold[12-24-48])//如 size > 临界值,就扩容
    resize(); 
    afterNodeInsertion(evict); 
    return null;
}


    5. 关于树化(转成红黑树)
    //如果 table 为 null ,或者大小还没有到 64,暂时不树化,而是进行扩容.
    //否则才会真正的树化 -> 剪枝
    final void treeifyBin(Node<K,V>[] tab, int hash) { 
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
    resize();
    }
        */
	}
}

HashTable介绍

HashMap和HashTable比较 

 TreeSet

 

public class TreeSet_ {

    public static void main(String[] args) {

        //解读
        //1. 当我们使用无参构造器,创建 TreeSet 时,仍然是无序的
        //2. 这里希望添加的元素,按照字符串大小来排序
        //3. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
        // 并指定排序规则
        //4. 简单看看源码
        //解读


        //TreeSet treeSet = new TreeSet();
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            //return调用字符串的compareTo方法进行字符串比较
            public int compare(Object o1, Object o2) {
                return ((String) o1).length() - ((String) o2).length();
            }
        });

        //解读
        /*
        1. 构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性 this.comparator
        public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
        }
        2. 在 调用 treeSet.add("tom"), 在底层会执行到
        if (cpr != null) {//cpr 就是我们的匿名内部类(对象)

        do {
        parent = t;
        //动态绑定到我们的匿名内部类(对象)compare
        cmp = cpr.compare(key, t.key);
        if (cmp < 0)
        t = t.left;
        else if (cmp > 0)
        t = t.right;
        else //如果相等,即返回 0,这个 Key 就没有加入
        return t.setValue(value);
        } while (t != null);
        }
         */
        //添加数据.
        treeSet.add("jack");
        treeSet.add("tom");//3
        treeSet.add("sp");
        treeSet.add("a");

        treeSet.add("abc");//3 不会添加进去 因为修改了compare方法,abc和tom长度相同 返回0
        System.out.println("treeSet=" + treeSet);//不做处理是无序输出
    }
}

TreeMap 

public class TreeMap_ {
    public static void main(String[] args) {
        //TreeMap treeMap = new TreeMap();
        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).length()-((String) o2).length();
            }
        });

        //使用默认的构造器,创建 TreeMap, 是无序的(也没有排序)
        /*
        要求:按照传入的 k(String) 的大小进行排序
        */
        treeMap.put("123",null);//key不能是null
        treeMap.put("jack", "杰克");
        treeMap.put("tom", "汤姆");
        treeMap.put("kristina", "克瑞斯提诺");
        treeMap.put("smith", "斯密斯");

        treeMap.put("hsp", "韩顺平");//如果把return改成String o1.length那么就会替换汤姆
        System.out.println("treemap=" + treeMap);
    }
}

Map 接口实现类-Properties 

1.Properties类继承了Hashtable类并实现了map接口,也使用一种键值对的方式存储数据

2.properties还可以用于从xxx.properties文件中,加载数据到properties类对象,并进行读取和修改,一般xxx.properties为配置文件(IO流中涉及)

3.使用方式与hashtable基本一致

public class HashTable {
    public static void main(String[] args) {

        //解读
        //1. Properties 继承 Hashtable
        //2. 可以通过 k-v 存放数据,当然 key 和 value 不能为 null
        //增加
        Properties properties = new Properties();
        //properties.put(null, "abc");//抛出 空指针异常
        //properties.put("abc", null); //抛出 空指针异常
        properties.put("john", 100);//k-v
        //底层初始化一个数组 Hashtable$Entry 是 Entry是Hashtable的内部类 初始化长度是11  临界值是11*0.75 = 8
        properties.put("lucy", 100);
        properties.put("lic", 100);
        properties.put("lic", 88);//如果有相同的 key , value 被替换
        System.out.println("properties=" + properties);
        //通过 k 获取对应值
        System.out.println(properties.get("lic"));//88
        //删除
        properties.remove("lic");
        System.out.println("properties=" + properties);
        //修改
        properties.put("john", "约翰");
        System.out.println("properties=" + properties);

        Hashtable hashtable = new Hashtable();
        hashtable.put("jack",123);
        System.out.println(hashtable);
        hashtable.remove("jack");
        System.out.println(hashtable);

        /**
         * 扩容机制
         * 当达到临界值的时候是在原来的基础上*2 + 1
         * 例如第一次是11 达到8之后 就是 11*2 + 1 = 23
         *
         */
    }
}

 总结:开发中如何选择集合类

1.判断存储类型,是一组对象(单列)还是键值对(双列)

2.一组对象(单列)Collection接口

​    允许重复:List

​                    增删多:LinkedList(双向链表,增加删除快)

​                    改查多:Arraylist

​    不允许重复:Set

​                    无序:HashSet

​                    排序:TreeSet

​                    插入和取出顺序一致:LinkedHashSet,底层维护数组和双向链表

3.一组键值对(双列)Map

​    键无序:HashMap

​    键排序:TreeSet

​    键插入和取出顺序一致:LinkedHashMap

​    读取文件:Properties

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值