Java基础知识(一)

1、List和Set的区别

List

有两种List,一种是基本的ArrayList,其优点在于随机访问元素,另一种是更强大的LinkedList,它并不是为快熟访问十五级的,而是具有一套更通用的方法。

Set

Set具有与Collection完全一样的接口,因此没有任何额外的功能。实际上Set就是Collection,只 是行为不同。这是继承与多态思想的典型应用:表现不同的行为。Set不保存重复的元素(至于如何判断元素相同则较为负责)

List和Set的区别

1.List,Set都是继承自Collection接口
2.List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
3.Set和List对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。                                                                                                                             4.List适合经常追加数据,插入,删除数据。但随机取数效率比较低。Set适合经常地随机储存,插入,删除。但是在遍历时效率比较低。

2、HashSet是如何保证不重复的?

首先HashSet有这么几个特点:

  • 无序
  • 无重复元素(因为基于HashMap实现的,后面有介绍)
  • 实现Set接口,由哈希表支持
  • 底层数据结构是哈希表
  • 如何保证元素唯一性
  • 要搞懂如何实现元素唯一性的,直接来看下HashSet如何添加元素的逻辑,源码如下:

HashSet.add方法

 /**
  * Adds the specified element to this set if it is not already present.
  * More formally, adds the specified element <tt>e</tt> to this set if
  * this set contains no element <tt>e2</tt> such that
  * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
  * If this set already contains the element, the call leaves the set
  * unchanged and returns <tt>false</tt>.
  *
  * @param e element to be added to this set
  * @return <tt>true</tt> if this set did not already contain the specified
  * element
  */
  public boolean add(E e) {
    return map.put(e, PRESENT)==null;
  }


map.put方法:

/**
 * Associates the specified value with the specified key in this map.
 * If the map previously contained a mapping for the key, the old
 * value is replaced.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with <tt>key</tt>, or
 *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
 *         (A <tt>null</tt> return can also indicate that the map
 *         previously associated <tt>null</tt> with <tt>key</tt>.)
 */
 public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
 }
 
 /**
  * Implements Map.put and related methods
  *
  * @param hash hash for key
  * @param key the key
  * @param value the value to put
  * @param onlyIfAbsent if true, don't change existing value
  * @param evict if false, the table is in creation mode.
  * @return previous value, or null if none
  */
 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;
 }


 可以看到,HashSet底层数据结构是HashMap,通过Map的put方法存储新元素;
1、put方法首先会通过hash(key)方法会判断key值中是否已存在,该方法具体如下

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2、如果key存在,则只会把新的value赋值给key(onlyIfAbsent属性默认false)从而保证了key的唯一性。
3、当我们使用Set集合都是需要去掉重复元素的,如果在存储的时候逐个equals()比较,效率较低,哈希算法提高了去重复的效率,降低了equals()的使用次数
当HashSet调用add()方法存储对象的时候,先调用对象的hashCode()方法,然后在集合中查找是否有哈希值相同的对象

  • 如果没有哈希值相同的对象就直接存入集合
  • 如果有哈希值相同的对象,就和哈希值相同的对象逐个进行equals()笔记,比较结果为false就存入,true则不存

4、将自定义类的对象存入HashSet去重复,类中必须重写hashcode()和equals()方法

  • hashcode():属性相同的返回值必须相同,属性不同的返回值尽量不同(提高效率)
  • equals():属性相同返回true,属性不同返回false,返回false的时候存储

3、HashMap是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?

HashMap是线程不安全的,其主要体现:
1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

参考链接:hashmap扩容线程安全问题_HashMap多线程扩容导致死循环(JDK1.7),为什么?
参考链接:为什么说 HashMap 不是线程安全的

4、HashMap的扩容过程。

1.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为<原下标+原容量>的位置

2.重新调整HashMap大小存在什么问题吗?

  • 当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。(多线程的环境下不使用HashMap)

  • 为什么多线程会导致死循环,它是怎么发生的?

  HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。
Resize是什么?首先我们先认识2个变量 
1.Capacity
  HashMap的当前长度。HashMap的长度是2的幂。
2.LoadFactor
  HashMap负载因子,默认值为0.75f。
衡量HashMap是否进行Resize的条件如下:
  HashMap.Size >= Capacity * LoadFactor
Resize步骤
  1.扩容:创建一个新的Entry空数组,长度是原数组的2倍。
  2.ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。
  hash公式:index = HashCode(Key) & (Length - 1)
我们假设rehash之前的HashMap是这样的

  

  那么rehash之后可能是这样

  

  代码是这样的  

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

/**

 * Transfers all entries from current table to newTable.

 */

void transfer(Entry[] newTable, boolean rehash) {

    int newCapacity = newTable.length;

    for (Entry<K,V> e : table) {

        while(null != e) {

            Entry<K,V> next = e.next;

            if (rehash) {

                e.hash = null == e.key ? 0 : hash(e.key);

            }

            int i = indexFor(e.hash, newCapacity);

            e.next = newTable[i];

            newTable[i] = e;

            e = next;

        }

    }

}

  现在假设一个场景,有一个hashmap如下

  

当有A,B这两个线程要对该hash map进行put操作,此时由于空间的不足,该hashmap必将进行扩容

假如此时线程B遍历到Entry3对象,刚执行完红框里的这行代码,线程就被挂起。对于线程B来说:
e = Entry3
next = Entry2
这时候线程A畅通无阻地进行着Rehash,当ReHash完成后,结果如下(图中的e和next,代表线程B的两个引用):

直到这一步,看起来没什么毛病。接下来线程B恢复,继续执行属于它自己的ReHash。线程B刚才的状态是:
e = Entry3
next = Entry2
我们继续执代码,Entry3放入了线程B的数组下标为3的位置,并且e指向了Entry2。此时e和next的指向如下:
e = Entry2
next = Entry2

 

接下来用头插法把Entry2插入到了线程B的数组的头结点
e = Entry2
next = Entry3

e = Entry3
next = Entry3.next = null

newTable[i] = Entry2这里若果是正常情况是newTable[i] =null,但是由于Entry2的hash被定为带同一个数组地址
e = Entry3
Entry2.next = Entry3
Entry3.next = Entry2
链表出现了环形!导致了死循环(多线程下请使用CocurrentHashMap)

转自:https://www.cnblogs.com/zhuoqingsen/

5、HashMap1.7与1.8的 区别,说明1.8做了哪些优化,如何优化的?

原文链接:https://blog.csdn.net/qq_36520235/article/details/82417949

不同点:

(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

(2)扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)

2、而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
这里写图片描述
在计算hash值的时候,JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。

扩容流程对比图:
这里写图片描述
(3)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)

这里写图片描述

这里在重新进行补充两个问题:(2019-09-03)

(1)为什么在JDK1.7的时候是先进行扩容后进行插入,而在JDK1.8的时候则是先插入后进行扩容的呢?

//其实就是当这个Map中实际插入的键值对的值的大小如果大于这个默认的阈值的时候(初始是16*0.75=12)的时候才会触发扩容,
//这个是在JDK1.8中的先插入后扩容
if (++size > threshold)
            resize();
  • 其实这个问题也是JDK8对HashMap中,主要是因为对链表转为红黑树进行的优化,因为你插入这个节点的时候有可能是普通链表节点,也有可能是红黑树节点,但是为什么1.8之后HashMap变为先插入后扩容的原因,我也有点不是很理解?欢迎来讨论这个问题?
  • 但是在JDK1.7中的话,是先进行扩容后进行插入的,就是当你发现你插入的桶是不是为空,如果不为空说明存在值就发生了hash冲突,那么就必须得扩容,但是如果不发生Hash冲突的话,说明当前桶是空的(后面并没有挂有链表),那就等到下一次发生Hash冲突的时候在进行扩容,但是当如果以后都没有发生hash冲突产生,那么就不会进行扩容了,减少了一次无用扩容,也减少了内存的使用
void addEntry(int hash, K key, V value, int bucketIndex) {
		//这里当钱数组如果大于等于12(假如)阈值的话,并且当前的数组的Entry数组还不能为空的时候就扩容
      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);
  }
 
 void createEntry(int hash, K key, V value, int bucketIndex) {
      Entry<K,V> e = table[bucketIndex];
    //把新加的放在原先在的前面,原先的是e,现在的是new,next指向e
      table[bucketIndex] = new Entry<>(hash, key, value, e);//假设现在是new
      size++;
  }

为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢(面试蘑菇街问过)?

  • 如果选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
  • 还有一点重要的就是由于treenodes的大小大约是常规节点的两倍,因此我们仅在容器包含足够的节点以保证使用时才使用它们,当它们变得太小(由于移除或调整大小)时,它们会被转换回普通的node节点,容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。所以作者应该是根据概率统计而选择了8作为阀值
    	//Java中解释的原因
       * Because TreeNodes are about twice the size of regular nodes, we
         * use them only when bins contain enough nodes to warrant use
         * (see TREEIFY_THRESHOLD). And when they become too small (due to
         * removal or resizing) they are converted back to plain bins.  In
         * usages with well-distributed user hashCodes, tree bins are
         * rarely used.  Ideally, under random hashCodes, the frequency of
         * nodes in bins follows a Poisson distribution
         * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
         * parameter of about 0.5 on average for the default resizing
         * threshold of 0.75, although with a large variance because of
         * resizing granularity. Ignoring variance, the expected
         * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
         * factorial(k)). The first values are:
         *
         * 0:    0.60653066
         * 1:    0.30326533
         * 2:    0.07581633
         * 3:    0.01263606
         * 4:    0.00157952
         * 5:    0.00015795
         * 6:    0.00001316
         * 7:    0.00000094
         * 8:    0.00000006
         * more: less than 1 in ten million

    (二)哈希表如何解决Hash冲突?

    这里写图片描述

    (三)为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化

    这里写图片描述

    (四)为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

    这里写图片描述

    (五)HashMap 中的 key若 Object类型, 则需实现哪些方法?

    这里写图片描述

6、final finally finalize区别

链接:https://blog.csdn.net/qq_38542085/article/details/90913810

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值