[JAVA]基础(二)集合

说说Java中常见的集合吧

Java中的常见集合可以概括如下:

  • Map接口和Collection接口是所有集合框架的父接口
  • Collection接口的子接口包括:Set接口和List接口
  • Map接口的实现类主要有:HashMap、TreeMap、Hashtable LinkedHashMap、ConcurrentHashMap以及Properties等
  • Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  • List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

容器中的设计模式

迭代器模式

  • Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素
  • 从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象

适配器模式

java.util.ArraysasList() 可以把数组类型转换为 List 类型,这就是适配器模式的应用

Map

HashMap

举例说明HashMap的线程不安全

  • HashMap线程不安全主要是考虑到了多线程环境下进行扩容可能会出现HashMap死循环
  • Hashtable线程安全是由于其内部实现在put和remove等方法上使用synchronized进行了同步,所以对单个方法的使用是线程安全的。但是对多个方法进行复合操作时,线程安全性无法保证。 比如一个线程在进行get操作,一个线程在进行remove操作,往往会导致下标越界等异常。

Java集合中的快速失败(fast-fail)机制:

  • 快速失败是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast

假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就可能会抛出 ConcurrentModificationException异常,从而产生fast-fail快速失败

Java集合fail-safe机制

安全失败机制

  • 采用安全失败机制的集合容器,在遍历时候不能再原集合内容上访问,而是复制原先集合内容,再拷贝的集合上进行遍历
  • 遍历过程对原集合所作的修改不会被迭代器检测到,不会抛出ConcurrentModificationException

HashMap底层实现结构有

HashMap底层实现数据结构为数组+链表的形式,JDK8及其以后的版本中使用了数组+链表+红黑树实现,解决了链表太长导致的查询速度变慢的问题

HashMap的初始容量,加载因子,扩容增量

HashMap的初始容量16,加载因子为0.75,扩容增量是原容量的1倍。
如果HashMap的容量为16,一次扩容后容量为32。HashMap扩容是指元素个数(包括数组和链表+红黑树中)超过了16*0.75=12之后开始扩容。

HashMap的长度为什么是2的幂次方

将一个键值对插入HashMap中,通过将Key的hash值与length-1进行&运算,实现Key的定位,2的幂次方可以减少冲突(碰撞)的次数,提高HashMap查询效率;加载因子,如果设置太大不利于空间利用设置太小则会导致碰撞增多,降低了查询效率,所以设置了0.75

HasMap的存储和获取原理

  • 当调用put()方法传递键和值来存储时,先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象,也就是找到了该元素应该被存储的桶中(数组)。当两个键的hashCode值相同时,bucket位置发生了冲突即Hash冲突,会在该bucket后边接上一个链表(JDK8及以后的版本中还会加上红黑树)来解决,将新存储的键值对放在表头(也就是bucket中)
  • 当调用get方法获取存储的值时,首先根据键的hashCode找到对应的bucket,然后根据equals方法来在链表和红黑树中找到对应的值==

HasMap的扩容机制

HashMap里面默认的负载因子大小为0.75,也就是说,当Map中的元素个数(包括数组,链表和红黑树中)超过了16*0.75=12之后开始扩容。将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
多线程环境下,HashMap扩容可能会导致死循环多线程下Rehash会造成元素之间形成一个循环链表

解决Hash冲突的方法有哪些?

  • 拉链法 (HashMap使用的方法)
  • 线性探测再散列法
  • 二次探测再散列法
  • 伪随机探测再散列法

哪些类适合作为HashMap的键?

String和Interger这样的包装类很适合做为HashMap的键,因为他们是final类型的类,而且重写了 equals()和hashCode()方法,避免了键值对改写,有效提高HashMap性能

HashMap和Hashtable的区别有哪些?

  • 底层实现不同
    HashMap在1.7后由数组链表红黑树实现;HashTable

  • 初始容量和扩容机制不同
    HashMap默认16,每次扩容默认为原容量2倍,制定容量时也会在满足2的幂次方
    HashTable默认11,每次扩容为默认原来2n+1也可扩容至指定容量

  • 对NUll的支持
    HashMap支持Null作为Key(但只能一个)和Value(可有多个)
    HashTable不支持Null键,会报NUllPointerException

  • 线程安全性
    HashMap线程不安全(可以使用Vector,ConcurrentHashMap或者用Collections的线程同步函数转换该HashMap)
    HashTable线程安全(底部方法加上了synchronied修饰)

  • 效率
    HashMap线程不安全效率高
    HashTable线程安全效率低,基本淘汰不适用

ConcurrentHashMap

ConcurrentHashMap和Hashtable的区别

底层实现

ConcurrentHashMap 1.7底层采用分段数组+链表实现,1.8的实现与HashMap一样(数组是主体,链表为了解决哈希冲突)

线程安全实现

Hashtable在每次同步执行时都要锁住整个结构
ConcurrentHashMap锁的方式是稍微细粒度

ConcurrentHashMap

Java1.7

,分段锁对整个桶数组分段(segment),每把锁只锁容器一部分,多线程访问不同数据段不存在锁紧竞争,提高并发效率

Java1.8

ConcurrentHashMap采用Node数组+链表+红黑树实现,并发控制由Synchronied和CAS实现

HashTable

使用同一把锁,Synchronized保证线程安全,效率低,同步代码块同一时刻只能一个线程访问锁竞争激烈

单线程环境下可以使用HashMap,多线程环境下可以使用ConcurrentHashMap,至于Hashtable已经不被推荐使用了

ConcurrentHashMap线程安全底层实现

  • ConcurrentHashMap由Segment数组和HashEntry数组实现
  • Segment数组实现ReentrantLock作为锁存在,
  • HashEntry数组用于存储键值对
  • 每个Segment守护一个HashEntry数组里得元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment锁。

Java1.7

将数组分段多个Segment,每段配一把锁,多线程访问不同段不存在锁竞争问题

  • ConcurrentHashMap由Segment数组和HashEntry数组实现
  • Segment数组实现ReentrantLock作为锁存在,
  • HashEntry数组用于存储键值对
  • 每个Segment守护一个HashEntry数组里得元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment锁

Java1.8

取消了分段锁,采用CAS和Synchred保证并发安全,其中Synchronized只锁定当前链表或二叉树(链表长度大于8)的首节点,只要不发生hash冲突就不会产生并发

TreeMap

TreeMap底层使用红黑树实现,TreeMap中存储的键值对按照键来排序

  • 如果Key存入的是字符串等类型,那么会按照字典默认顺序排序

  • 如果传入的是自定义引用类型,比如说User,那么该对象必须实现Comparable接口,并且覆盖其compareTo方法;或者在创建TreeMap的时候,我们必须指定使用的比较器

      	// 方式一:定义该类的时候,就指定比较规则
      class User implements Comparable{
          @Override
          public int compareTo(Object o) {
              // 在这里边定义其比较规则
              return 0;
          }
      }
      public static void main(String[] args) {
          // 方式二:创建TreeMap的时候,可以指定比较规则
          new TreeMap<User, Integer>(new Comparator<User>() {
              @Override
              public int compare(User o1, User o2) {
                  // 在这里边定义其比较规则
                  return 0;
              }
          });
      }
    

Comparable接口和Comparator接口有哪些区别

  • Comparable实现比较简单,但是当需要重新定义比较规则的时候,必须修改源代码,compareTo(Object obj1)即修改实体类里的compareTo方法
  • Comparator接口不需要修改源代码,只需要在创建TreeMap的时候重新传入一个具有指定规则的比较器即可,用compare(Object obj1,Object obj2)排序

List

ArrayList的扩容机制

ArrayList的扩容是通过

  1. 将原先的数组复制到另一个内存空间更大的数组
  2. 把新元素添加到扩容以后的数组

查看ArrayList的源码可以知道

  • ArrayList提供了无参构造,此时创建空数组,有参构造,按照传入的参数构建数组
  • 扩容的实现add(E,e)方法
    • 增加长度 ensureCapacityInternal(int minCapacity)
    • 添加元素到数组 elementData[size++] = e

ensureCapacityInternal(int minCapacity)方法

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
  • calculateCapacity(elementData, minCapacity),如果空数组则最小容量取默认容量的minCapacity之间自大值

       private static int calculateCapacity(Object[] elementData, int minCapacity) {
              //如果传入的是个空数组则最小容量取默认容量与minCapacity之间的最大值
              if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                  return Math.max(DEFAULT_CAPACITY, minCapacity);
              }
              return minCapacity;
          }
    
  • ensureExplicitCapacity方法可以判断是否需要扩容

      private void ensureExplicitCapacity(int minCapacity) {
                modCount++;
       
                // 如果最小需要空间比elementData的内存空间要大,则需要扩容
                if (minCapacity - elementData.length > 0)
                    //扩容
                    grow(minCapacity);
            }
    
  • ArrayList扩容的关键方法grow()
    计算出原数组的容量,扩容至原来1.5倍,在调用Arrays.copyOf将elementData数组拷贝到新的数组

      private void grow(int minCapacity) {
                // 获取到ArrayList中elementData数组的内存空间长度
                int oldCapacity = elementData.length;
               // 扩容至原来的1.5倍
               int newCapacity = oldCapacity + (oldCapacity >> 1);
               // 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组,
                // 不够就将数组长度设置为需要的长度
               if (newCapacity - minCapacity < 0)
                   newCapacity = minCapacity;
               //若预设值大于默认的最大值检查是否溢出
               if (newCapacity - MAX_ARRAY_SIZE > 0)
                   newCapacity = hugeCapacity(minCapacity);
               // 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
               // 并将elementData的数据复制到新的内存空间
               elementData = Arrays.copyOf(elementData, newCapacity);
           }
    

ArrayList和LinkedList有哪些区别?

  • ArrayList底层使用了动态数组实现,实质上是一个动态数组
  • LinkedList底层使用了双向链表实现,可当作堆栈、队列、双端队列使用
  • ArrayList在随机存取方面效率高于LinkedList
  • LinkedList在节点的增删方面效率高于ArrayList
  • ArrayList必须预留一定的空间,当空间不足的时候,会进行扩容操作
    LinkedList的开销是必须存储节点的信息以及节点的指针信息
  • Vector,它是线程安全的ArrayList,但是已经被废弃,不推荐使用了。多线程环境下,我们可以使用CopyOnWriteArrayList替代ArrayList来保证线程安全

HashSet和TreeSet有哪些区别?

  • HashSet底层使用了Hash表实现
    保证元素唯一性的原理:判断元素的hashCode值是否相同。如果相同,还会继续判断元素的equals方法,是否为true

    HashSet的底层实现是HashMap,只使用了其中的Key,

    • HashSet的add方法底层使用HashMap的put方法将key = e,value=PRESENT构建成key-value键值对,当此e存在于HashMap的key中,则value将会覆盖原有value,key保持不变,所以将一个已经存在的e元素添加中HashSet中,不会保存到HashMap中,满足HashSet中元素不会重复。
    • HashSet的contains方法使用HashMap得containsKey方法实现
  • TreeSet底层使用了红黑树来实现
    保证元素唯一性是通过Comparable或者Comparator接口实现

HashMap与HashSet区别

  • 底层实现
    HashSet底层由HashMap实现

  • 存储不同
    HashMap存储键值对,put
    HashSet只存对象,add

  • 去重
    HashMap使用Key计算hashCode
    HashSet 使用成员对象计算hashCode,相同时还要调用equasl判断相等

  • 实现接口不同

HashSet如何去重

把对象加入HashSet先计算其hashcode判断对象当前加入位置并与其他对象hashcode比较,没有相同hashcode则加入Set,否则会调用equals方法检查是否相同,相同则不可加入Set

LinkedHashMap和LinkedHashSet区别

  • LinkedHashMap可以记录下元素的插入顺序和访问顺序
  • 通过构造方法public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder), accessOrder传入true可以实现LRU缓存算法(访问顺序
  • LinkedHashSet 底层使用LinkedHashMap实现,类似与HashMap和HashSet的关系

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

List和Set的区别

  • List是有序的并且元素是可以重复的
  • Set是无序(LinkedHashSet除外)的,并且元素是不可以重复的
    (此处的有序和无序是指放入顺序和取出顺序是否保持一致)

Iterator和ListIterator的区别是什么?

  • Iterator可以遍历list和set集合;ListIterator只能用来遍历list集合
  • Iterator前者只能前向遍历集合;ListIterator可以前向和后向遍历集合
  • ListIterator其实就是实现了前者,并且增加了一些新的功能

数组和集合List之间的转换

数组和集合Lis的转换在我们的日常开发中是很常见的一种操作,主要通过Arrays.asList以及List.toArray方法来搞定

package niuke; 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
 
public class ConverTest {
    public static void main(String[] args) {
        // list集合转换成数组
        ArrayList<String> list =  new ArrayList<>();
        list.add("zhangsan");
        list.add("lisi");
        list.add("yangwenqiang");
        Object[] arr = list.toArray();
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println("---------------");
        // 数组转换为list集合
        String[] arr2 = {"niuke", "alibaba"};
        List<String> asList = Arrays.asList(arr2);
        for (int i = 0; i < asList.size(); i++) {
            System.out.println(asList.get(i));
        }
    }
}

Collection和Collections区别

Array和Arrays
  • Array 数组类,Java中最基本的一个存储结构, 提供了动态创建和访问 Java 数组的方法。其中的元素的类型必须相同。

  • Arrays 此静态类专门用来操作array ,提供搜索、排序、复制等静态方法。

    equals():比较两个array是否相等,array拥有相同元素个数,且所有对应元素两两相等。

    sort():用来对array进行排序。

    binarySearch():在排好序的array中寻找元素。

    Arrays.asList(array):将数组array转化为List

Executor和Executors

  • Executor 接口对象能执行我们的线程任务
  • ExecutorService 接口继承了Executor接口并进行了扩展,提供了更多的方法,我们能够获得任务执行的状态并且可以获取任务的返回值。
  • Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求

有哪些集合是线程不安全,解决

不安全集合

  • ArrayList
  • HashMap
  • HashSet
  • PriorityQueue

安全集合

JUC下有许多并发容器可以选择

  • ConcurrntHashMap(线程安全HashMap)
  • CopyOnWriteArrayList(线程安全ArrayList)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值