java集合类

1 篇文章 0 订阅

集合是什么?

Java集合类存放于 java.util 包中,是一个用来存放对象的容器

注意
• 集合只能存放对象。
• 集合存放的是多个对象的引用,对象本身还是放在堆内存中
• 集合可以存放不同类型,不限数量的数据类型。定义集合变量时如果不指定数据类型,则默认数据类型为Object

Iterator迭代器

走访器,可以理解为集合中元素的指针
它是Java集合的顶层接口(不包括map系列的集合,Map接口是map系列集合的顶层接口)

Iterator类定义

public interface Iterator<E> {
boolean hasNext();   判断容器内是否还有可供访问的元素
E next();返回迭代器刚越过的元素的引用,返回值是 Object,需要强制转换成自己需要的类型

写法

  while(lt.hasNext()){
         Object obj=lt.next();
}

Collection接口

无序、允许重复
Collection有两个子接口: List和Set

类定义:

public interface Collection<E> extends Iterable<E>{}
// 一般说Collection是集合框架的顶级接口,但是事实上并不是顶级接口

主要方法:

  • int size(); 获取集合中的元素个数

    Collection cc=new ArrayList();
    for (int i = 0; i < 10; i++) {
    cc.add(i);
    System.out.println(cc.size());
    }
    
  • boolean isEmpty()判断集合中的元素个数是否为0
    注意:只判断是否没有元素,但是并不判断集合对象是否为null

  • boolean contains(Object o)用于判断集合中是否包含对象o

  • Iterator iterator();获取集合的迭代器对象,用于实现对元素的遍历操作

    Collection cc=new ArrayList<>();
    Random r=new Random();
    for (int i = 0; i < 10; i++) 
    	cc.add(r.nextInt(100));
    Iterator it=cc.iterator();
    while (it.hasNext()) {
    	Object temp=it.next();
    	System.out.println(temp);
    }
    
  • Object[] toArray(); 将集合转换为数组

  • boolean add(E e)用于将元素e对象添加到集合中

  • boolean remove(Object o)从集合中删除指定的元素o

  • void clear();清空集合

List接口

集合有序、允许重复

它在Collection接口基础之上添加了每个元素的索引编号,编号从0开始,到size-1结束

接口定义

public interface List<E> extends Collection<E> {}

主要方法

  • boolean add(E e);向集合末尾追加元素e对象

    List list=new ArrayList();
    list.add(100);
    list.add(101);
    System.out.println(list.size());
    Iterator it=list.iterator();
    while (it.hasNext()) {
    	Object obj=it.next();
    	System.out.println(obj);
    }
    

    在这里插入图片描述

  • boolean remove(Object o)删除第一个和o相等的元素,如果没有和o相等元素,则报异常IndexOutOfBoundsException

  • default void sort(Comparator<? super E> c) {} 接口中提供了默认的排序实现,这里需要提供一个比较器Comparator

    源代码:

    @SuppressWarnings({"unchecked", "rawtypes"})
     default void sort(Comparator<? super E> c) {
    	Object[] a = this.toArray();
    	Arrays.sort(a, (Comparator) c);
    	ListIterator<E> i = this.listIterator();
    	for (Object e : a) {
    	    i.next();
    	    i.set((E) e);
    	 }
      }
    

    示例:

    List list = new ArrayList();
    Random r = new Random();
    for (int i = 0; i < 10; i++) 
    	list.add(r.nextInt(100));
    list.forEach(System.out::println);
    //需要使用sort方法必须是同类型可比较
    list.sort(new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
    	if (o1 != null && o2 != null) {
    		if (o1 instanceof Integer) {
    			Integer k1 = (Integer) o1;
    			if (o2 instanceof Integer) {
    				Integer k2 = (Integer) o2;
    					return k1 - k2;
    			}
    		}
    	}
    	return 0;
    }});
    System.out.println("==============");
    list.forEach(System.out::println);
    
    

特殊方法

和序号相关

  • E get(int index); 获取指定索引号对应的元素,index应该在[0,size-1],若没有指定的位置,则抛出IndexOutOfBoundsException异常
  • E set(int index, E element); 用于给指定索引位置上进行赋值,这个位置上必须有对应的数据(已经赋过值),这里实际上是修改操作,否则IndexOutOfBoundsException
  • void add(int index, E element); 向指定索引位置index上添加元素element,原始数据自动后移
  • E remove(int index);删除指定位置的元素
  • int indexOf(Object o); 查找从左向右第一个o元素的下标索引,如果元素不存在返回-1
  • int lastIndexOf(Object o);从右向左查找

常见的List接口的实现类

• ArrayList:数组实现,查询快,增删慢,轻量级;(线程不安全)

• LinkedList:双向链表实现,增删快,查询慢 (线程不安全)

• Vector:数组实现,重量级 (线程安全、使用少)

ArrayList的使用和实现
List list=new ArrayList();
public class ArrayList<E> extends AbstractList<E>  implements List<E>, RandomAccess, Cloneable, java.io.Serializable

具体数据存储使用的是transient Object[] elementData;//transient的含义为不能被序列化

构造器

无参构造器为

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

这里的常量为

   private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

使用无参构造器时ArrayList会构建一个空数组(长度为0)用于未来存放数据,这里是一种对内存消耗的优化处理

带参构造器 例如:List list=new ArrayList(12) 12就是初始化容积,这里的初始化参数值必须为(0,int的最大值)范围内

 public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
        }
    }
主要方法
  1. add方法的定义:

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
    private void ensureCapacityInternal(int minCapacity) {
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }
    
      private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
    
    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    
       private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
        }
    

    结论:添加数据时当存储数据的数组长度不足时,数组会自动变长,变长的比例为50%,新的数组的长度为原来的1.5倍

  2. remove方法

      public boolean remove(Object o) {
            if (o == null) {
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        fastRemove(index);
                        return true;
                    }
            } else {
                for (int index = 0; index < size; index++)
                    if (o.equals(elementData[index])) {
                        fastRemove(index);
                        return true;
                    }
            }
            return false;
        }
    
     private void fastRemove(int index) {
            modCount++;
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work
        }
    

    结论:使用数组元素移动的方式实现元素的删除
    注意:这里没有变小容积
    修改元素个数时会有modCount的修改

  3. get方法的实现

     public E get(int index) {
            rangeCheck(index);
    
            return elementData(index);
        }
    
      private void rangeCheck(int index) {
            if (index >= size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    

    结论:首先要求index应该在[0,size-1]的范围内,否则异常
    如果index正确则按照下标从数组中获取元素

LinkedList的使用和实现
public class LinkedList<E>extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable

参数:

  • transient int size = 0;用于记录集合中的元素个数
  • transient Node<E> first;这里就是双向链表的实现中的头指针
  • transient Node<E> last;这里就是双向链表的实现中的尾指针

Node的定义:

private static class Node<E> {
        E item;   //在节点上存储的数据
        Node<E> next;   //指向下一个Node元素
        Node<E> prev;  //指向上一个Node元素

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

构造器
-public LinkedList() { }

没有LinkedList(int)构造器
这里可以知道:数组需要进行初始化容积;但是链表不需要初始化容积,实际存储多少个元素就创建多少个Node节点对象

Vector 实现原理

@since JDK1.0 是一个古老的实现类

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

内部的数据存储采用的是Object[]

 protected Object[] elementData;

参数 - protected int elementCount; 用于表示当前数组中存放的元素个数======size

构造器
  1. 默认构建一个10个长的数组

       public Vector() {
            this(10);
        }
    
  2. 可以指定一个整数作为初始化容积,默认容积增长步长值为0

     public Vector(int initialCapacity) {
            this(initialCapacity, 0);
        }
    
  3. 可以指定一个整数作为初始化容积,第二个整数是容积增长步长值

      public Vector(int initialCapacity, int capacityIncrement) {
            super();
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            this.elementData = new Object[initialCapacity];
            this.capacityIncrement = capacityIncrement;
        }
    
主要方法

add(Object)新加元素

 public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

同步方法:线程安全性,但是并发要受到限制

 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        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);
    }

如果容积需要增长则增加100%

remove(Object)

    public boolean remove(Object o) {
        return removeElement(o);
    }
    public synchronized boolean removeElement(Object obj) {
        modCount++;
        int i = indexOf(obj);
        if (i >= 0) {
            removeElementAt(i);
            return true;
        }
        return false;
    }
public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                 elementCount);
    }
    else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    elementCount--;
    elementData[elementCount] = null; /* to let gc do its work */
}

也为同步方法,线程安全

常见的List接口的实现类总结

ArrayListLinkedListVector
实现方式数组,按照索引下标访问速度快O(1),但是当删除添加元素时会导致元素的移动,速度慢O(n)双向链表,按照索引下标访问速度慢O(n),但是删除添加元素速度快O(1)数组,按照索引下标访问速度快O(1),但是当删除添加元素时会导致元素的移动,速度慢O(n)
是否同步不同步,线程不安全,但是并发高,访问效率高不同步,线程不安全,但是并发高,访问效率高同步,所以线程安全,但是并发低,访问效率低
如何选择经常需要快速访问,较少在中间增加删除元素时使用;如果多线程访问,则需要自行编程解决线程安全问题经常需要在内部增删元素,但是很少需要通过索引快速访问时使用;如果多线程访问,则需要自行编程解决线程安全问题一般不使用,如果在多线程访问时可以考虑使用

Set接口

集合无序,不允许重复

扩展Collection接口

接口定义

public interface Set<E> extends Collection<E> {}

允许使用null元素

这里没有什么新方法,只对 add()、equals() 和 hashCode() 方法添加了限制

HashSet和TreeSet是Set的实现

Set—>hashSet --> linkedHashSet

SortedSet —> TreeSet

主要方法

  • boolean add(E e);向集合中追加元素e对象,如果出现重复则后盖前

实现类

HashSet:equals返回true,hashCode返回相同的整数;哈希表;存储的数据是无序的。
LinkedHashSet:此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。存储的数据是有序的。

HashSet类

HashSet类直接实现了Set接口, 其底层其实是包装了一个HashMap去实现的。

 private transient HashMap<E,Object> map;

HashSet采用HashCode算法来存取集合中的元素,因此具有比较好的读取和查找性能。

向set中添加元素到底比较是采用==还是equals?

Set<String> set=new HashSet<String>();
String s1="abc";
String s2=new String("abc");
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
set.add(s1);
set.add(s2);
System.out.println(set.size());	
运行结果:
false
true
1

因此向set中添加元素采用的是equlas()方法进行比较

向HashSet中添加元素时首先执行的是对象的hashcode值比较,如果两个对象的hashcode值相等时才会继续调用equals方法;如果两个对象的hashcode值不相等则不会调用equals方法

HashSet实际上是通过使用HashMap的key实现的,所有key对应的value都是一个常量

HashSet的特征

无序:不仅不能保证元素插入的顺序(如果需要顺序则可以使用LinkedHashSet),而且在元素在以后的顺序中也可能变化(这是由HashSet按HashCode存储对象(元素)决定的,对象变化则可能导致HashCode变化)
如果需要访问的顺序和插入的顺序一致,可以使用HashSet的子类LinkedHashSet
不允许重复 [equals和hashcode]

如何判断两个对象相等 --HashSet的equals和hashCode
  • 实现原理
    实现Set接口的HashSet,依靠HashMap来实现的。
    我们应该为要存放到散列表的各个对象定义hashCode()和equals()。

HashSet需要同时通过equals和HashCode来判断两个元素是否相等,具体规则是,如果两个元素通过equals为true,并且两个元素的hashCode相等,则这两个元素相等(即重复)。

所以如果要重写保存在HashSet中的对象的equals方法,也要重写hashCode方法,重写前后hashCode返回的结果相等(即保证保存在同一个位置)。所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。

结论:要求当两个同类型对象equals为true时,必须hashCode值一致。当HashSet判定对象重复时,首先调用的是对象的hashCode方法,如果两个对象的hashCode值相同时,才调用equals进行判定。如果hashCode值不相等则不会调用equals判断。如果 hashcode相等而且equals为true,则后盖前

HashSet是线程非安全的,方法上没有同步约束

HashSet元素值可以为NULL

HashSet常用方法

构造器

    public HashSet() {
        map = new HashMap<>();
    }

HashSet 的后台有一个HashMap;初始化后台容量;只不过生成一个HashSet的话,系统只提供key的访问; 如果有两个Key重复,那么会覆盖之前的;

  • public boolean contains(Object o) :如果set包含指定元素,返回true

  • public Iterator iterator()返回set中元素的迭代器

  • public Object[]toArray() :返回包含set中所有元素的数组

  • public Object[] toArray(Object[] a) :返回包含set中所有元素的数组,返回数组的运行时类型是指定数组的运行时类型

  • public Boolean add(Object o) :如果set中不存在指定元素,则向set加入

  • public boolean remove(Object o) :如果set中存在指定元素,则从set中删除

LinkedHashSet的特征
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable {}

LinkedHashSet是HashSet的一个子类,LinkedHashSet也根据HashCode的值来决定元素的存储位置,但同时它还用一个链表来维护元素的插入顺序,插入的时候即要计算hashCode又要维护链表,而遍历的时候只需要按链表来访问元素

因为底层采用链表和哈希表的算法。链表保证元素的添加顺序,哈希表保证元素的唯一性

TreeSet类
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable{}
public interface NavigableSet<E> extends SortedSet<E> {}

TreeSet实现了SortedSet接口,这是一种排序的Set集合

数据存储采用的是

 private transient NavigableMap<E,Object> m;
public interface NavigableMap<K,V> extends SortedMap<K,V> {}

查看jdk源码发现底层是用TreeMap实现的,本质上是一个红黑树原理。正因为它是排序了的,所以相对HashSet来说,TreeSet提供了一些额外的按排序位置访问元素的方法,例如first(), last(), lower(), higher(), subSet(), headSet(), tailSet().
TreeSet的排序分两种类型,一种是自然排序,另一种是定制排序

典型题目

使用Set集合删除List集合中的重复值

//使用Set集合删除List集合中的重复值
public class Test {
	public static void main(String[] args) {
		Random r=new Random();
		List< Integer> list=new ArrayList<Integer>();
		for (int i = 0; i < 20; i++) 
			list.add(r.nextInt(100));			
		System.out.println(list);
		Set<Integer> set=new LinkedHashSet<Integer>(list);
		System.out.println(set);
	}
}

几种Set的比较

HashSet: 不保证元素的添加顺序,底层采用哈希表算法,查询效率高。判断两个元素是否相等,equals()方法返回为true,要求hashCode()值必须相等。即要求存入HashSet中的元素要覆盖equlas()方法和hashCode()方法。

LinkedHashSet是Hashset的子类,底层采用了哈希表算法及链表算法,既保证了元素的添加顺序,也保证了查询效率,但是整体性能要低于HashSet。

TreeSet不保证元素的添加顺序,但是会对集合中的元素进行排序。底层采用红黑树算法(树比较适合查询),但是添加的效率低。

Collection和Collections的区别

Collection是java.util下的接口,它是各类集合的父接口,继承于它的接口主要有List和Set;Collections是java.util下的类,是针对集合的帮助类,提供一系列的静态方法实现对各种集合的搜索、排序、线性安全性等操作。

Map接口

在介绍Map接口前首先了解一下哈希表

哈希表

哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。如果所有的键(hashCode)都是整数,那么就可以使用一个简单的无序数组(Set)来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。

它提供了一组键值的映射。其中存储的每个数据对象都有一个相应的键key,键决定了值对象在Map中的存储位置。键应该是唯一的,不允许重复,每个key 只能映射一个value。

Map接口的定义

public interface Map<K,V> {}

定义map对象时需要指定key和value对应的类型,必须是复杂类型,不能使用简单类型(例如int)

内部接口定义

 interface Entry<K,V> {}

map接口中有一个内部接口为Entry

内部接口的主要方法:
K getKey();
V getValue();
V setValue(V value);
结论:每个Entry对象中封装了一个key和value

boolean equals(Object o); int hashCode();

另外Entry接口中通过静态方法提供了一组比较器的默认实现

用于规范其中存放数据的规则,map中所存放的数据将给封装为一个一个的Entry,一个key/value对对应一个Entry对象

Map接口中常见方法

  • Object put(Objectkey,Object value):用来存放一个键-值对,Map中 如果出现key值冲突则后盖前。允许key值和value值为null,但是key值为null只能有一个,value值为null没有个数限制
  • size():int用于获取集合中的元素个数
  • Object remove(Object key):根据key(键),移除键-值对,并将值返回
  • Object get(Object key) :根据key(键)取得对应的值,如果key值不存在则返回为null
  • boolean containsKey(Object key) :判断Map中是否存在某键(key)
  • void clear() :清空当前Map中的元素
  • boolean containsValue(Object value):判断Map中是否存在某值(value)
  • public Set keySet() :返回所有的键(key),并使用Set容器存放,获取key值后就可以通过get方法获取key对应的值value
  • public Collection values() :返回所有的值(Value),并使用Collection存放
  • public Set entrySet() :返回一个实现 Map.Entry 接口的元素 Set
  • forEach和Lambda表达式实现输出

3种迭代访问方式:

public class Test {
	public static void main(String[] args) {
		//key要求String类型,value要求Integer类型
		Map<String, Integer> map=new HashMap<String, Integer>();
		for (int i = 0; i < 10; i++) 
			//按照key值存储数据,key要求唯一,如果重复则后盖前,value没有特殊要求
			map.put("key"+i, i);
		//如何进行遍历访问
		//1、依赖key值进行遍历
		Set<String> keys=map.keySet();//获取所有的key值
		Iterator<String> it=keys.iterator();
		while (it.hasNext()) {
			String key=it.next();
			Integer value=map.get(key);
			System.out.println(key+"---->"+value);
		}
		
		//2、直接遍历value,注意不能直接通过value获取key
		Collection<Integer> vals=map.values();//获取所有的value
		Iterator<Integer> it2=vals.iterator();	
		while (it2.hasNext()) {
			Integer value=it2.next();
			System.out.println("----"+value);
			
		}
		//3、获取所有的Entry
		Set<Entry<String, Integer>> sets=map.entrySet();
		Iterator<Entry<String, Integer>> it3=sets.iterator();
		while (it3.hasNext()) {
			Entry<String, Integer> temp=it3.next();
			String key=temp.getKey();
			Integer value=temp.getValue();
			System.out.println(key+"--->"+"value");
		}	
	}
}

Map实现类

HashMap、TreeMap、LinkedHashMap、Hashtable等

HashMap存储键值对,key不能重复(keySet():Set),但是value可以重复(values():Collection);key的实现就是HashSet;value对应着放;允许null的键或值;HashMap是线程不安全的,在多线程环境下,使用Hashmap进行put操作会引起死循环(可能出现环形链的情况),导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

作为key值出现的类型必须实现equals和hashCode两个方法,要求equals为true,则hashcode码必须相等

Hashtable:线程安全的,不允许null的键或值(否则NullPointerException);是线程安全的但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁。多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。

Properties:key和value都是String类型,用来读配置文件;

TreeMap:对key排好序的Map; key 就是TreeSet, value对应每个key; key要实现Comparable接口或新定义的比较器对象

LinkedHashMap: 此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。存储的数据是有序的。

ConcurrentHashMap主要就是为了应对hashmap在并发环境下不安全而诞生的,ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响。ConcurrentHashMap避免了对全局加锁改成了局部加锁操作,这样就极大地提高了并发环境下的操作速度

HashMap

HashMap类定义
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {}
具体的内部数据存储方式

两个重要的阈值

static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;

当链表结构上节点个数大于等于8并且最小树的容积大于等于64时进行树化处理,自动转换为红黑树,以提高查询效率(O(log2n))

 static final int UNTREEIFY_THRESHOLD = 6;

当红黑树上节点个数小于等于6时自动退化为链表结构

存储数据- transient Node<K,V>[] table;

静态内部类用于实现Entry接口,HahMap中存放的key/value对就被封装为Node对象。其中key就是存放的键值,用于决定具体的存放位置;value是具体存放的数据,hash就是当前Node对象的hash值,next用于指向下一个Node节点(单向链表)

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

Java的官方文档建议我们初始化哈希表时尽量指定哈希表的容量,以避免哈希表扩容带来的性能消耗。

/**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

HashMap()采用所有的默认配置值,其中的参数值有initialCapacity:int初始化容积,默认值DEFAULT_INITIAL_CAPACITY=16,第2个参数是loadFactor:float用于设置加载因子,取值范围为0-1之间,默认值为DEFAULT_LOAD_FACTOR=0.75,值越大则存储数据越多,hash冲突的概率越高;值越小则越浪费空间,但是hash冲突的概率越低。为了减少hash冲突,引入了加载因子,加载因子表示容器中最多存储百分子多少的数据。例如默认容器为16,默认加载因子为0.75,则表示最多存储16*0.75个元素,如果大于这个值则需要进行扩容

 /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

参数是初始化容积

 /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

两个参数,分别是初始化容积(应该在1到2^30之间)和加载因子值(0到1之间)

    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

获取2n<初始化容积<2(n+1)时返回2(n+1),返回一个大于等于初始化容积值的2n值

tableSizeFor用于计算容积值,不是设置多少就是多少。就是计算一个2^n值>=设定的容积值.例如初始化容积参数值为7则实际创建容积值为8

注意:在构造器中并没有创建任何用于存储数据的集合—延迟加载,第一次存储数据时才进行空间分配

HashMap的存储结构

每个Node[]数组中的元素被称一个桶bucket,一个桶对应一个hash映射的值,例如0,1等,可能会出现不同的key,但是hash映射的位置相同,例如16、32等,这采用链表结构存储hash映射值相同的所有数据(JDK8在单个链表长度大于阈值(8)时自动转换为红黑树,删除节点使某单个链表节点数小于阈值(6)时会自动从红黑树退化为链表结构)

相关参数:
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor

在这里插入图片描述

HashMap底层采用的是Entry数组和单向链表实现。

Map 主要用于存储键key值value对,根据键得到值,因此键不允许重复,但允许值重复。

HashMap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。

HashMap最多只允许一条记录的键为null;允许多条记录的值为null

HashMap不支持线程的同步,即任一时刻可以有多个线程同时HashMap;可能会导致数据的不一致,会出现环形链。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力。

如何判断环型链?
在一个hashmap中的所有元素挨个取出来,放到一个set里,每次往里边放元素时判断一下我取到的新元素在set里是否存在,如果存在,则出现了环型链(相当于把一个元素读两次才出现了环型链),map里的key是唯一的。

在这里插入图片描述
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。
TREEIFY_THRESHOLD为 8如果新插入的值是链表中的第 9 个会触发下面的 treeifyBin(树化操作,就是将单向链转换为红黑树),也就是将链表转换为红黑树。
JDK8+插入数据到链表的最后面,Java7 是插入到链表的最前面

HashMap实现原理—散列

Hash哈希算法的意义在于提供了一种快速存取数据的方法,它用一种算法建立键值与真实值之间的对应关系。散列表又称为哈希表。

散列表算法的基本思想是:以结点的关键字为自变量,通过一定的函数关系(散列函数)计算出对应的函数值,以这个值作为该结点存储在散列表中地址。

在JDK1.8之前,HashMap采用数组+单向链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。

而JDK1.8中,HashMap采用数组+单向链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。单向链表的查询时间复杂度为O(n),红黑树查询的时间复杂度为O(log2N)

HashMap中的put方法的实现

在这里插入图片描述

键类型要求equlas()和hashcode()同时重写

常用的hashcode解决方法有四种

  • 开放地址法
  • 再hash的方法
  • 拉链法;java的解决方法
  • 建立公共溢出区法
要点总结
  • 结构特点:Java 8-中的HashMap是基于“数组+链表”的方式(链表法解决冲突),到了Java 8,应该是“数组+链表/红黑树”的方式,默认阈值为树化8,退化6.
  • 线程安全:HashMap是不安全,并发可能会导致环形链,Collections Framework中有两种线程安全的实现:Collections.synchronizedMap (new java.util.HashMap<>());和ConcurrentHashMap,前者是锁整个表,后者是16个分段锁
  • 性能特点:HashMap可以在常数时间内增加,删除,查找元素,但这也是一种平均情况,使用load factor装载因子计算阈值就是为了减少冲突带来的性能退化;
    HashMap的桶数组一次扩展为原数组的2倍,控制扩展和移动的次数,这里需要执行rehash计算
HashMap并发问题
  • put可能出现元素丢失
  • 并发访问出现死循环,出现环型链(同一个HashMap在做put操作可能导致两个或两个以上线程做rehash动作)
HashMap三种并发解决方案

1、HashTable替换HashMap
2、Collections.SynchronizedMap将HashMap包装起来
3、ConcurrentHashMap替换HashMap

HashTable

public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {

Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。

存储数据- private transient Entry<?,?>[] table;

Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。通过"拉链法"实现的哈希表

Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。

在hashmap中允许key和value为null,只是key只能有一个null,如果出现冲突后盖前

Properties

public class Properties extends Hashtable<Object,Object> {}

Properties没有泛型约束,但是key和value都是String类型,用来读配置文件(.properties资源文件、xml配置文件);

Properties文件是java中很常用的一种配置文件,文件后缀为“.properties”,属文本文件,文件的内容格式是“键=值”的格式,可以用“#”作为注释,java编程中用到的地方很多,运用配置文件,可以便于java深层次的解耦

创建资源文件(编码方式必须为utf-8),可以和java文件位置相同,也可以定义在其它位置(编码麻烦)

在资源文件定义配置信息,定义格式规则为: key=value,注意这里不要出现其它符号,如果出现其它符号会被识别为配置内容 Key=value # 注意这里没有其他的符号

常用方法
  • setProperty(String key, String value) 调用 Hashtable 的方法 put。
  • getProperty(String key) 用指定的键在此属性列表中获取属性值
    getProperty(String key, String defaultValue) 用指定的键在属性列表中获取属性值,若键不存在则返回默认值defaultValue。
  • load(InputStream inStream) 从输入字节流中读取属性列表(键和元素对)
  • load(Reader reader) 按简单的面向行的格式从输入字符流中读取属性列表(键和元素对)。
  • loadFromXML(InputStream in) 将指定输入流中由 XML 文档所表示的所有属性加载到此属性表中。
  • store(OutputStream out, String comments) 以适合使用 load(InputStream) 方法加载到 Properties 表中的格式,将此 Properties 表中的属性列表(键和元素对)写入输出流。
  • store(Writer writer, String comments) 以适合使用 load(Reader) 方法的格式,将此 Properties 表中的属性列表(键和元素对)写入输出字符。
  • storeToXML(OutputStream os, String comment) 发出一个表示此表中包含的所有属性的 XML 文档。
  • storeToXML(OutputStream os, String comment, String encoding) 使用指定的编码发出一个表示此表中包含的所有属性的 XML 文档。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值