List、Set、Map总结

本文详细介绍了Java集合框架中的List、Set、Map的基本概念和特性,包括ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap、LinkedHashMap和Stack。同时,深入解析了HashMap的put和get方法的源码,强调了在使用自定义类作为键时重写hashCode和equals方法的重要性。

《Java编程思想第四版》读后感:
这里写图片描述

文章分两部分:

一、List、Set、Map基本介绍
二、理解hashCode,和对hashMap的put和get源码分析。


一、List、Set、Map基本介绍

1、List和Set继承了Collection接口,而Map并不是。
2、图中虚线实心黑色箭头,是指可以通过某个方法得到所指向的接口。
例如:

ListIterator listIterator = list.listIterator();
Collection values = map.values();

3、ArrayList:保证元素按照规定的顺序排列,可以添加重复的数据。不是线程安全的,要保证线程安全建议使用Vector,在addElement()等方法添加了synchronized[’sɪŋkrənaɪzd] 。ArrayList的特点是提供了快速访问。
4、LinkedList在API中说明了是用双链表实现的高效率地在列表中部进行插入和删除操作,但是随机访问慢于ArrayList。
5、LinkedList和ArrayList都可以通过方法得到ListIterator.

ListIterator linkedListIterator = linkedList.listIterator();
        ListIterator arrayListIterator1 = arrayList.listIterator();

利用它可在一个列表里朝两个方向遍历,同时插入和删除位于列表中部的元素(插入和删除建议使用LinkedList)


6、HashSet:HashSet里面的数据是无序不可重复的并且如果放入的是一些继承Object的类,你应该重写equals和hashCode方法。
如果是String、Integer等类则不需要,以为在源码中已经给你写好了。
7、TreeSet:TreeSet是有序和不可重复的。对于TreeSet大致可分为自然排序和Comparator进行排序。
从TreeSet的构造函数中可以看到:

// 默认构造函数。使用该构造函数,TreeSet中的元素按照自然排序进行排列。
TreeSet()

// 创建的TreeSet包含collection
TreeSet(Collection<? extends E> collection)

// 指定TreeSet的比较器
TreeSet(Comparator<? super E> comparator)

// 创建的TreeSet包含set
TreeSet(SortedSet<E> set)
  • 自然排序:对于一些基本数据类型的包装类例如Integer,String等等。其实这些类里面都已经实现了Comparable接口。
  • Comparator:如果我们在集合中添加的是我们自己写的类,那么就应该实现Comparator接口,并重写compareTo方法。
    如果不实现这个接口会抛出
Exception in thread "main" java.lang.ClassCastException: com.example.MyType cannot be cast to java.lang.Comparable
    at java.util.TreeMap.compare(TreeMap.java:1294)
    at java.util.TreeMap.put(TreeMap.java:538)
    at java.util.TreeSet.add(TreeSet.java:255)
    at com.example.Set2.main(Set2.java:49)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
class MyType implements Comparable {
    private String name;
    private int i;

    public MyType(int n, String name) {
        i = n;
        this.name = name;
    }


    public String toString() { return i + " "; }
    //在TreeSet添加类对象时必须重写该方法,自然排序不用,因为自然排序String、Integer中已经重写好了这个方法
    public int compareTo(Object o) {
        System.out.println("compareTo");
        int i2 = ((MyType) o).i;
        return (i2 < i ? -1 : (i2 == i ? 0 : 1));
    }
}
  public static void main(String[] args) {
        TreeSet treeSet=new TreeSet();
        for (int i=0;i<10;i++){
            treeSet.add(new MyType(i,""+i));
        }
        treeSet.add(new MyType(0,""+100));
        System.out.println("treeSet = " + treeSet);
    }

输出:treeSet = [9 , 8 , 7 , 6 , 5 , 4 , 3 , 2 , 1 , 0 ]
虽然在main函数中添加了两次0,但是输出仍然为一次,因为在比较语句中(i2 < i ? -1 : (i2 == i ? 0 : 1));中,大于的放在前面,小于的放在后面,而等于0的就不插入,所以重复的没有插入进去。

TreeSet实际上是TreeMap实现的。
TreeSet是非线程安全的。
源码片段:

private static final Object PRESENT = new Object();
public TreeSet() {
    this(new TreeMap<E,Object>());
}
// 添加e到TreeSet中
    public boolean add(E e) {
         return m.put(e, PRESENT)==null;
}

8、LinkedHashSet:内部使用散列以加查询速度,同时使用链表维护元素的次序。也应该重写hashCode和equals方法。

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet=new LinkedHashSet();
        Set fill = Set2.fill(linkedHashSet, 10);
        Set set = Set2.fill(fill, 10);
        System.out.println("set = " + set);
    }
} ///:~
public class MyType {
    private int i;

    public MyType(int n) {
        i = n;
    }

    public boolean equals(Object o) {
        System.out.println("MyType.equals");
        return (o instanceof MyType) && (i == ((MyType) o).i);
    }

    public int hashCode() {
        System.out.println("MyType.hashCode");
        return i;
    }

    public String toString() {
        return i + " ";
    }


} ///:~

set = [0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ]


9、HashMap:是一种键值对结构,当将自己写的类作为键时,应该重写hashCode和equals方法,跟HashSet差不多。
10、TreeMap:是一种键值对结构,当将自己写的类作为键时,应该实现Comparable接口,跟TreeSet差不多。
11、LinkedHashMap:是一种键值对结构,可以按照插入的顺序存放,也可以按照LRU算法(最近最少使用)进行存放。

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        LinkedHashMap linkedHashMap=new LinkedHashMap();
        linkedHashMap.put("A","a");
        linkedHashMap.put("B","b");
        linkedHashMap.put("C","c");
        linkedHashMap.put("D","d");
        System.out.println("linkedHashMap = " + linkedHashMap);
        linkedHashMap=new LinkedHashMap(16, 0.75f, true);
        linkedHashMap.put("A","a");
        linkedHashMap.put("B","b");
        linkedHashMap.put("C","c");
        linkedHashMap.put("D","d");
        System.out.println("linkedHashMap = " + linkedHashMap);
        linkedHashMap.get("B");
        linkedHashMap.get("A");

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

    }
} 

输出:

linkedHashMap = {A=a, B=b, C=c, D=d}
linkedHashMap = {A=a, B=b, C=c, D=d}
linkedHashMap = {C=c, D=d, B=b, A=a}

没有被访问过的(可被看做需要删除的)元素就会出现在队列的前面。

使用LinkedHashMap 也应该重写hashCode和equals方法。

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        LinkedHashMap linkedHashMap=new LinkedHashMap();
        Map map = Set2.fill(linkedHashMap, 10);
        Map fill = Set2.fill(map, 10);
        System.out.println("fill = " + fill);
    }
} 
public class MyType implements Comparable {
    private int i;

    public MyType(int n) {
        i = n;
    }

    public boolean equals(Object o) {
        System.out.println("MyType.equals");
        return (o instanceof MyType) && (i == ((MyType) o).i);
    }

    public int hashCode() {
        System.out.println("MyType.hashCode");
        return i;
    }

    public String toString() {
        return i + " ";
    }

} ///:~

fill = {0 =0, 1 =1, 2 =2, 3 =3, 4 =4, 5 =5, 6 =6, 7 =7, 8 =8, 9 =9}


12、Stack:Stack有时也可以称为”后入先出”(LIFO)集合。换言之,我们在堆栈里最后”压入”的东西将是以后第一个”弹出”的。

public class Stacks {
    static String[] months = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};

    public static void main(String[] args) {
        Stack stk = new Stack();
        for (int i = 0; i < months.length; i++)
            stk.push(months[i] + " ");
        System.out.println("stk = " + stk);
        // Treating a stack as a Vector:
        stk.addElement("The last line");
        System.out.println("element 5 = " + stk.elementAt(5));
        System.out.println("popping elements:");
        while (!stk.empty())
            System.out.println(stk.pop());



    }
}

输出:

stk = [January , February , March , April , May , June , July , August , September , October , November , December ]
element 5 = June 
popping elements:
The last line
December 
November 
October 
September 
August 
July 
June 
May 
April 
March 
February 
January 

13、hashMap和HashTable区别:

  • hashMap的键可以接受为null,当接受为null的时候默认插入的位置为0。
  • hashTable的键和值不能为null,否则会报错。
  • hashTable是线程安全的,hashMap不是线程安全的。
  • 可以通过Map m = Collections.synchronizeMap(hashMap);返回同步的hashMap。

二、理解hashCode,和对hashMap的put和get源码分析。

这里写图片描述

其实从HashMap源码中看每一个Item都是一个HashMapEntry,这是一个静态的类,里面维持了一个单向链表的结构。

  static class HashMapEntry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        HashMapEntry<K,V> next;
        int hash;
}

这个HashMapEntry保存了key和value以及hash值,并且next指向下一个HashMapEntry。

在源码中:

    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;

在程序的运行中会初始化table:
table = new HashMapEntry[capacity];

当put的时候:

 public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

首先HashMap允许key-value为null,当key为null时

 private V putForNullKey(V value) {
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

当key为null时,会取table的第0的元素(HashMapEntry),然后进行遍历。如果e不等于null,如果key等于null。就将新的值把旧的值替换掉,并且返回。
如果table[0]这个位置为null就会跳到addEntry方法(hash=0,key=null,数组下标为0)

 void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

在这里又重新计算了bucketInex(数组下标)

 static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

这里因为hash传进的为0,所以返回的还是0,所以取到的还是第0个元素。
最后调用createEntry方法:

  void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        size++;
    }

这里e为null。并且在table[0]的位置上创建了一个新的HashMapEntry,并通过构造函数将next指向了e。

 HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

这里在回过头来看:

  for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

这里的步骤跟上面差不多,就不赘述了,但是这里会调用hash方法和equals方法,所以我们在平时使用的时候应该重写这两个方法。

get方法:

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
 final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

如果put方法懂了,那么get方法自然而然也懂了。
主要还是根据hash值,求出来数组的坐标,然后取出坐标所对应的链表,进行遍历,并根据hash值和equals方法,返回对应的value。

微信公众号:
这里写图片描述

QQ交流群:365473065

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值