JAVA面试(一):基础篇

二分查找

目标

  • 掌握手写二分查找的代码及细节
  • 快速解答二分查找的选择题

编写二分查找的代码

  1. 前提:有已排好序的数组A
  2. 定义左边界L,右边界R,确定搜索范围,循环执行二分查找(3,4两步)
  3. 获取中间索引值M=Floor((L+R)/2) <向下取整>
  4. 中间索引的值A[M]与待搜索的值T进行比较
    • A[M]==T 表示找到,返回中间索引
    • A[M]>T ,中间值右侧的其他元素都大于T,无需比较,中间索引左边去找,M-1设置为右边界,重新查找
    • A[M]<T ,中间值右侧的其他元素都小于T,无需比较,中间索引右边去找,M+1设置为左边界,重新查找
  5. 当L>R时,表示没有找到,应结束循环
	/**
     * 二分查找
     * @param a 数组
     * @param t 待查找数
     * @return 如果找到,则返回元素索引,否则返回-1
     */
    public static int binarySearch(int[] a,int t){
        int l = 0;//左下标
        int r = a.length-1;//右下标
        int z; //中间值
        while (l<=r){
            z = (l+r)/2;
            if(a[z] == t) {
                return z;
            }else if(a[z]>t){
                r = z-1;
            }else if(a[z]<t){
                l = z+1;
            }
        }
        return -1;
    }

获取中间索引时,如何避免整数溢出

  • 改变求中间值的公式
z = ( l + r ) / 2  
 => l / 2 + r / 2  
  => l + ( -l / 2+ r / 2)
    => l + ( r- l ) / 2 
  • 无符号的右移运算代替除法
    右移运算:右移一位相当于对该数进行除2的运算
    当相加后的数的数值位上超出至符号位时,右移一位刚好将其数值位恢复
z = (l + r)>>>1;

相关面试题

    1. 有一个有序表1,5,8,11,19,22,31,35,40,45,48,49,50 当二分查找值为48的节点时,查找成功需要比较的次数
      4次
    1. 使用二分法在序列 1,4,6,7,15,33,39,50,64,78,75,81,89,96中查找元素81时,需要经过( 4 )次的比较
    1. 在已经排序的128个数组中查找一个数,需要比较次数最多不超过多少次
      7次
  • 奇数二分取中间

  • 偶数二分取中间靠左

  • 2的n次方 = 128 或 128/2/2…=1,共有几次

  • 问题转化为log以2为底128,==> 用log以10为底128/log以10为底2(计算器)

排序

目标

  • 掌握常见的排序算法(快排,冒泡,选择,插入等)的实现思路
  • 手写冒泡,快排的代码
  • 了解各个排序算法的特性,如时间复杂度,是否稳定

冒泡排序

	/**
     * 冒泡排序
     * @param a
     */
    private static void bubble(int[] a) {
        //当数组已经有序时,可以提前退出循环
        //外层循环控制冒泡轮数
        for(int j = 1;j<a.length;j++) {
            //比较次数
            //该循环控制一轮冒泡,
            boolean s = true;
            for (int i = 0; i < a.length - j; i++) {
                //每次相邻元素进行比较
                if (a[i] > a[i + 1]) {//大的数往后移
                    s = false;
                    swap(a, i, i + 1);
                }
            }

            System.out.println(j+"轮    " + Arrays.toString(a));
            if(s) break;
        }

    }

    /**
     * 交换两个数在数组中的位置
     * @param a 数组
     * @param i 下标
     * @param j 下标
     */
    public static void swap(int []a,int i,int j){
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

当原本数组有已排好序的元素时,如5,4,1,7,3,8,9 当一轮冒泡过后,4,1,5,3,7,8,9 后三位已有序
改进:记录最后一个交换的左边元素的下标(例子中,最后一次交换时3和7的交换,3的下标可作为下一轮冒泡的结束)

 /**
     * 冒泡排序-->优化版
     * @param a
     */
    private static void bubble(int[] a) {
        //当数组已经有序时,可以提前退出循环
        //外层循环控制冒泡轮数
        int index = a.length-1;
        int lastIndex = a.length-1;
        for(int j = 1;j<a.length;j++) {
            //比较次数
            //该循环控制一轮冒泡
            for (int i = 0; i < index; i++) {
                //每次相邻元素进行比较
                if (a[i] > a[i + 1]) {//大的数往后移
                    lastIndex = i;
                    swap(a, i, i + 1);
                }
            }
            index = lastIndex ;

            System.out.println(j+"轮    " + Arrays.toString(a));
            if(index == 0) break;
        }

    }

文字描述(以升序为例)

  1. 一次比较数组中相邻的两个元素大小,若a[j]>a[j+1] ,则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后
  2. 重复以上步骤,直到整个数组有序

优化方式

每轮冒泡时,最后一次交换索引可以作为下一轮冒泡的比较次数,如果这个值为零,表示整个数组有序,直接退出外层循环即可

选择排序

代码实现


/**
 * @author wilihelmi
 */
public class SelectionSort {
    public static void main(String[] args) {
        int []a = {5,3,7,2,1,9,8,4};
        selection(a);
        System.out.println(Arrays.toString(a));
    }

    /**
     * 选择排序
     * @param a 待排序数组
     */
    public static void selection(int a[]){
        //循环比较在未排序的元素中选择最小的
        for(int i = 0;i<a.length-1;i++){
            int s = i;//未排序的元素最小值的下标
            for(int j = i+1;j<a.length;j++){
                //比较每一个剩余元素与当前选定的最小值
                if(a[j]<a[s]){
                    s = j;
                }
            }
            //比较完成后,s下标所对应的元素即为剩余未排序元素的最小值

            if(s!=i){
                //交换
                int t = a[s];
                a[s] = a[i];
                a[i] = t;
            }

        }
    }
}

文字描述

  1. 将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选取最小的元素,放入排序的子集
  2. 重复以上步骤,知道整个数组有序

与冒泡排序比较

  1. 二者平均时间复杂度都是O( n 2 n^2 n2)
  2. 选择排序一般要快于冒泡,因为其交换次数少
  3. 但如果集合有序度高,冒泡优先选择
  4. 冒泡是属于稳定排序的算法,而选择是属于不稳定的排序

插入排序

代码实现


/**
 * @author wilihelmi
 */
public class InsertSort {
    public static void main(String[] args) {
        int []a = {5,3,7,2,1,9,8,4};
        insert(a);
        System.out.println(Arrays.toString(a));
    }

    /**
     * 插入循环
     * @param a
     */
    public static void insert(int []a){
    //i代表待插入元素的索引
        for(int i =1;i<a.length;i++){
            int temp = a[i];//待插入元素的值
            int j = i-1;//已排序元素的索引
            while (j>=0){
                if(a[j]>temp){
                    a[j+1] = a[j];//后移
                }else {
                    break;
                }
                j--;
            }
            a[j+1] =temp;
            System.out.println(Arrays.toString(a));
        }
    }
}

文字描述

  1. 将数组分为两个区域,排序区和未排序区域,每一轮从未排序区域中选取出第一个元素,插入到排序区域(需要保证顺序)
  2. 重复以上步骤,直到整个数组有序

优化方式

  1. 待插入元素进行比较时,遇到比自己小的元素,就代表找到了插入位置,无需进行后续比较
  2. 插入时可以直接移动元素,而不是交换元素

与选择排序比较

  1. 两者平均复杂度都是O( n 2 n^2 n2)
  2. 大部分情况,插入排序略优于选择排序
  3. 有序集合插入排序的时间复杂度为O(n)
  4. 插入排序属于稳定算法,而选择排序不属于稳定排序

插入和选择

分别使用插入排序和选择排序算法,对序列18,23,19,9,23,15进行排列。第三趟排序后的结果为

  1. 选择排序
    • 9,23,19,18,23,15(选择最小元素9与下标为0的元素交换)
    • 9,15,19,18,23,23
    • 9,15,18,19,23,23
  2. 插入排序
    • 18,23,19,9,23,15 (下标为1的元素值,经过判断在原位置不变化)
    • 18,19,23,9,23,15
    • 9,18,19,23,23,15

快速排序

  1. 每一轮排序选择一个基准点(pivot)进行分区
    1. 让小于基准点的元素进入一个分区,大于基准点的进入另一个分区
    2. 当分区完成时,基准点元素的位置就是其最终的位置
  2. 在子分区内重复以上过程,直至子分区元素个数少于等于1,这体现了分而治之的思想

实现方式

  1. 单边循环快排
    1. 选择最右边的元素作为基准点元素
    2. j 指针负责找到比基准小的元素,一旦找到则与 i 进行交换
    3. i 指针维护小于基准点元素的边界,也是每次进行交换大的目标索引
    4. 最后基准点与i交换,i即为分区位置
  2. 双边循环快排
    1. 选择最做元素作为基准点元素
    2. j指针负责从右向左找比及转电销的元素,i指针负责从左向右找比基准点大的元素,一旦找到两者交换,直至i,j相交
    3. 最后基准点与此时的i交换,i即为分区位置
  3. 双边循环要点
    1. 基准点在左边,并且要先移动j再移动i
    2. while(i<j){//当i>j时,退出循环
      //j 从右找小的元素
      while(i<j&&a[j]>pv){//当j小于等于基准点时退出(同时判断i<j是防止在判断过程中违反外部循环)
      j–;
      }
    3. while(i<j&&a[i]<=pv){//当j小于基准点时退出(因为i指针从基准点出发,若设置为a[i]<pv,则相等时不满足条件,i指针不进行更改,则会改变基准点的下标)
      i++;
      }
/**
 * @author wilihelmi
 * 单边循环
 */
public class QucikSort {
    public static void main(String[] args) {
        int []a = {5,3,7,2,9,8,1,4};
        quick(a,0,a.length-1);
        System.out.println(Arrays.toString(a));
    }

    public static void quick(int[]a,int l,int h){
        if(l>=h){
            return;
        }
        int p = partition(a, l, h);//第一次分区后基准值的索引值
        quick(a,l,p-1);
        quick(a,p+1,h);
    }

    /**
     * 单边循环--快速排序
     * 通过该方法,可以将所有的比基准点小的元素移至基准点左边,比基准点大的元素移至基准点的右边,确定基准点的正确位置
     * @param a 待排序数组
     * @param l 分区的上边界
     * @param h 分区的下边界
     * @return 代表的是基准点元素所在的正确索引,确定下轮上下边界
     */
    public static int partition(int[] a,int l, int h){
        int pv =a[h];//基准点选择最右边的元素
        int i = l;//i指针用于维护比基准点小的元素的边界
        for(int j = l;j<h;j++){//j指针用于寻找比基准点小的元素
            if(a[j]<pv){
                //将比基准点小的元素都移动到i指针的左边
                int temp = a[j];
                a[j] = a[i];
                a[i] = temp;
                i++;
            }
        }
        if(i!=h) {
            int temp = a[h];
            a[h] = a[i];
            a[i] = temp;
        }
        return i;
    }
}

/**
 * @author wilihelmi
 * 双边循环
 */
public class QuickSort1 {
    public static void main(String[] args) {
        int []a = {5,3,7,2,9,8,1,4};
        quick(a,0,a.length-1);
        System.out.println(Arrays.toString(a));
    }

    public static void quick(int[]a,int l,int h){
        if(l>=h){
            return;
        }
        int p = partition(a, l, h);//第一次分区后基准值的索引值
        quick(a,l,p-1);
        quick(a,p+1,h);
    }

    /**
     * 进行排序
     * @param a 待排数组
     * @param l 最左边下标
     * @param h 最右边下标
     * @return
     */
    public static int partition(int[] a,int l,int h){
        int pv = a[l];//选择最左边元素作为基准点
        int i = l;//寻找大于基准点的指针
        int j = h;//寻找小于基准点的指针
        while(i<j){//当i>j时,退出循环
            //j 从右找小的元素
            while(i<j&&a[j]>pv){//当j小于等于基准点时退出(同时判断i<j是防止在判断过程中违反外部循环)
                j--;
            }
            //i 从左找大的元素
            while(i<j&&a[i]<=pv){//当j小于基准点时退出(因为i指针从基准点出发,若设置为a[i]<pv,则相等时不满足条件,i指针不进行更改,则会改变基准点的下标)
                i++;
            }
            //交换i和j的值
            int temp =a[i];
            a[i]=  a[j];
            a[j]=temp;
        }
        //交换j和l的值,将基准点元素移至
        int temp =a[i];
        a[i]=  a[l];
        a[l]=temp;

        return j;
    }
}

特点

  1. 平均时间复杂度是O(n$\log 2 2 2$ n ),最坏时间复杂度O( n 2 n^2 n2)
  2. 数量比较大时,优势十分明显
  3. 属于不稳定排序

集合

ArrayList

  • 目标
    1. 掌握ArrayList 的扩容机制
    2. 掌握lterator的fail-fast,fail-sate机制

扩容机制

  • ArrayList()会使用长度为零的数组
  • ArrayList(int initialCapacity)会使用指定容量的数组
  • ArrayList(Collection <? exatenfd E> c) 会使用c的大小作为数组容量
    ·
  • add(Object o) 首次扩容为10,再次扩容为上次容量的1.5倍(对原数组长度进行移位运算[]除2?],再与原数组长度相加即为扩容数组长度)【该情况是对于没有指定数组长度的数组,对已经有长度的数组而言,扩容即扩容为1.5倍】
    eg:
    1. 原集合没有元素,在调用add时,数组扩容长度为10
    2. 原集合有元素,在调用add时,扩容为原数组的1.5倍
  • addAll(Collectionc)没有元素时,扩容为Math.max(10,实际元素个数)【选择较大的数】,有元素时为Math.max(原容量的1.5倍,实际元素个数)
    eg:
    1. 原集合没有元素,在调用addAll时【选择:添加的元素个数,10 中的较大值】
      • 添加的元素个数若大于10,则使用实际元素个数作为扩容数组长度;
      • 添加的元素个数若小于10,则使用10作为扩容数组长度
    2. 原集合有元素,在调用addAll时【选择:添加的元素个数加上原元素个数和,原元素个数的1.5倍 中的较大值】
      • 添加的元素个数加上原元素个数若大于原元素个数的1.5倍,则使用添加的元素个数加上原元素个数作为扩容数组长度;
      • 添加的元素个数加上原元素个数若小于原元素个数的1.5倍,则使用添原元素个数的1.5倍作为扩容数组长度;

Iterator

遍历集合时,若对集合元素进行修改,不同的集合有什么不同的应对措施呢?
fail-fast:一旦发现遍历的同时其他人来修改,则立刻抛异常;ArrayList是fail-fast的典型代表,遍历的同时不能修改,尽快失败。
fail-safe:发现遍历的同时其他人来修改,应当能有应对策略,例如牺牲一些一致性来让整个遍历运行完成(遍历时更改,遍历继续,按照未更改进行遍历);CopyOnWriteArrayList是fail-safe的典型代表,遍历的同时可以修改,原理是读写分离。

fail-fast

  • ArrayList通过继承获得一个变量modCount,在调用集合更改(增减,删除等)操作时,该变量会发生变化
  • 在遍历集合ArrayList时,创建其内部类Itr (继承于Iterator)对象,该迭代器有一变量expectedModCount 初始化时值为modCount的值
  • 遍历期间方法,迭代器方法会检查集合modCount值与迭代器expectedModCount 是否一致
  • 若不一致,则会抛出异常ConcurrentModificationException(并发性异常)

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
	protected transient int modCount = 0;
}

public class ArrayList<E> extends AbstractList<E>//ArrayList类继承AbstractList类
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	//无论是调用add还是remove方法,
	//对于ArrayList从AbstartList中继承来的变量modCount都是递增的,
	//该变量是记录集合更改次数的
	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);
    }
    
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        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

        return oldValue;
    }



	//1. 在执行循环时,会构建一个迭代器
	//2. 调用内部类的构造方法
	public Iterator<E> iterator() {
        return new Itr();
    }
    
 	private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;//****
		
		 //3. 创建该类的对象时,
		 //其变量expectedModCount 初始化值为该集合的更改数值
        Itr() {}
        
        //在遍历集合,执行next方法时,会优先检查
        public E next() {
            checkForComodification();//调用这个方法检查
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
		//判断modCount[集合更改次数]和expectedModCount[创建迭代器时集合的更改次数]是否一致
		//判断该集合是否被更改
		//若更改,抛出ConcurrentModificationException异常
		final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
        
	}
}

fail-safe

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    private transient volatile Object[] array;//该集合存储元素的数组
	
	//获取该集合存储元素的数组
	final Object[] getArray() {
        return array;
    }
	
	//设置该集合存储元素的数组
    final void setArray(Object[] a) {
        array = a;
    }

	//构造方法
	public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

	//添加方法
	public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//获取该集合的数组
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//创建新的数组,并复制原先数组元素至新数组
            newElements[len] = e;//将新元素添加
            setArray(newElements);//将新数组设置为该集合的数组
            return true;
        } finally {
            lock.unlock();
        }
    }

	//在增强for循环中,调用该方法创建内部类对象(继承)
	public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);//传入参数
    }

	static final class COWIterator<E> implements ListIterator<E> {
		//该数组初始指向集合中的数组,
		//若集合在遍历过程中发生变化,则集合数组指向新创建的数组,
		//该迭代器中依旧指向原来的数组,
		//故两个数组数据不同步,但迭代可机组进行
        private final Object[] snapshot; 
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
}

LinkedList

ArrayList vs LinkedList

  • ArrayList
    1. 基于数组,需要连续存储
    2. 随机访问快(根据下标进行访问)
    3. 尾部插入,删除性能可以,其他部分插入删除都会移动数据,因为性能会低
    4. 可以利用cpu缓存,局部性原理
  • LinkedList
    1. 基于双向链表,无需连续内存
    2. 随机访问慢(要沿着链表遍历)
    3. 头尾插入删除性能高
    4. 占用内存多
  • ArrayList头部插入较慢,尾部插入快,LinkedList头尾插入较快,中间插入较慢(将指针移至中间位置较为耗费时间)

HashMap

底层数据结构,1.7和1.8有何不同?

1.7 数组+链表
1.8 数组+(链表|红黑树)

为什么要用红黑树呢,为何一上来不树化,树化阈值为何是8,何时会树化,何时会退化为链表?

    • 只用数组+链表,当链表过长时,会影响查找性能;
    • 红黑树用来避免DoS攻击,防止链表超长时影响性能下降,树化应该是偶然情况
    • 当链表比较短时,其查找性能优于红黑树;只有到链表的长度较长时,红黑树的查找性能才会优于链表;且树占用的内存大于链表;
    • hash表的查找,更新的时间复杂度是O(1),而红黑树的查找,更新的时间的时间复杂度是O(log 以2为底的n),TreeNode占用空间也比普通Node的大,如果非必要,尽量还是使用链表
  1. hash值如果够随机,则在hash表内按柏松分布,在负载因子0.75的情况下,长度超过8的链表出现概率是0.00000006,选择8就是为了让树化几率足够小
  2. 要满足数组长度大于等于64,且链表长度大于阈值8;
    • 退化情况1:在扩容时如果拆分树时,分化后的树元素个数<=6则会退化为链表
    • 退化情况2:remove树节点前,进行检查,若root,root.left,root.right,root.left.left(树的根节点,根节点的左孩子,右孩子,左孙子)有一个为null,移除该节点,树会退化为链表

存入一对键值对:对其键值求哈希值,原始hash和二次hash,对二次hash值根据数组容量进行求模运算,计算桶下标;实现元素的快速查找。
链表过长的解决----扩容:当添加某键值对,添加到某个桶中,Map中元素个数大于数组容量的0.75倍(加载因子,扩容因子)时,或者某个桶内元素超8时,则进行扩容(数组长度两倍递增);当不同元素的hash值模数组长度获得的桶下标一致但其hash值不同时,当数组长度改变,其不同元素的桶下标会发生变化,故链表的长度可能减短
链表过长的解决----树化

  1. 数组容量必须大于64时,才可能会树化,否则优先考虑对数组进行扩容,减短链表长度
  2. 当数组容量必须大于64且链表元素长度大于树化阈值8时,才会树化

索引如何计算?hashCode都有了,为何还要提供hash()方法?数组容量为何会是2的n次幂?

  1. 索引由对元素键值进行hashCode运算后,再进行二次hash,即HashMap的hash()方法进行运算,得到的结果对数组容量进行求模运算
key原始hash二次hash桶下标
a97971

数组初始长度为16
97 % 16 ⇒ 1
97 & (16-1) ⇒1
求模运算底层为除法运算,比较麻烦,可换算为按位与运算(该情况仅适用于数组长度为2的倍数时)
97 % 64 ⇒ 33
97 & (64-1) ⇒ 33

  1. 二次hash()是为了综合高位数据,让哈希分布更为均匀;hashCode()求下标,只有低位数据参与运算,高位数据的不同不会影响下标的结果,造成更多冲突,则分布不均匀
  2. 计算索引时,如果是2的n次幂可以使用位与运算代替取模,效率更高;扩容时 hash & oldCap(旧容量)==0的元素留在原来位置,否则更新位置 = 旧位置 + oldCap;
  3. 上述都是为了配合容量为2的n次幂时的优化手段,例如hashTable的容量就不是2的n次幂,并不能说哪种设计更优,应该是设计者综合了各种因素,最终选择了2的n次幂作为容量(当容量为一个较大的质数时,分布会更加均匀)

介绍一下put方法流程,1.7和1.8有何不同?

  • 流程
    1. HashMap是懒创建,首次使用才创建数组
    2. 计算索引(桶下标)
    3. 如果桶下标害没人占用,创建Node占位并返回
    4. 如果桶下标已被占用
      • 已经是TreeNode走红黑树的添加或更新逻辑
      • 是普通Node,走链表的添加或更新逻辑,如果链表长度超过阈值(且数组长度大于64),走树化逻辑
    5. 返回前检查容量是否超出阈值,一旦超出进行扩容
  • 不同
    1. 链表插入节点时,1.7是头插法,1.8是尾插法
    2. 1.7是大于等于阈值没有空位时才扩容,1.8是大于阈值就扩容
    3. 1.8在扩容计算Node索引时,会优化

加载因子为何默认为0.75?

在空间占用与查询时间之间取得了较好的权衡;大于这个值,空间节省了,但是链表就会比较长影响性能;小于这个值,冲突减少了,但是扩容就会更为频繁,空间占用多。

多线程下操作HashMap会有什么问题?

  1. 扩容死链(1.7)
  2. 数据错乱(1.7,1.8)
    [eg:不同线程添加元素时,t1和t2两个线程放入map的元素,它们键值hash值一致,在put方法判断该数组位置是否已有节点时,两个线程都进入判断语句,此时均没有节点占用数组空间,同时将节点赋值给数组,则会覆盖元素]
扩容死链
  • 扩容元素迁移过程图示
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 多线程扩容死链形成过程图示
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
死链形成
在这里插入图片描述
在这里插入图片描述

key能否为null,作为key的对象有什么要求?

  1. HashMap的key可以为null,但Map的其他实现则不然,否则会出现空指针
  2. 作为key的对象必须实现hashCode和equals,并且key的内容不能修改(不可变)

String对象的hashCode()如何设计的,为啥每次乘的都是31?

  • 所有对象的hasshCode()设计的目的都是为了使散列更加均匀,每个字符串的hashCode都足够独特
  1. 字符串中的每一个字符都可以表现为一个数字,称为Si,其中i的范围时0~n-1
  2. 散列公式为:S0 * 31的(n-1)次方+S1* 31的(n-2)次方+···+Si * 31的(n-1-i)次方+···+Sn-1 * 31的0次方
  3. 31代入公式有比较好的散列特性,并且31 * h可以被优化为(便于计算)
    • 32 * h - h
    • 2的5次方 * h - h
    • h << 5 - h

设计模式

单例模式

目标

  • 掌握单例模式常见的五种实现方式
  • 了解jdk中哪些地方体现了单例模式

饿汉式

  1. 构造私有
  2. 提供私有静态成员变量(该类对象)
  3. 提供公共的静态方法(返回私有静态对象)

只要类初始化了,该对象就会创建,跟是否使用该对象无关,所以称为饿汉式

//饿汉式
public class Singleton1 implements Serializable {
    private Singleton1 (){
        //预防反射破坏单例
        if(INSTANCE !=null){
            throw new RuntimeException("单例对象已创建");
        }
        System.out.println("private Singleton1 ");
    }
    
    private static final Singleton1 INSTANCE=new Singleton1();
    
    public static Singleton1 getInstance(){
        return INSTANCE;
    }
    
    public static void otherMethod(){
        System.out.println("otherMethod()");
    }
    
    //预防反序列化破坏单例模式
    public Object  readResolve(){
        return INSTANCE;
    }
}

破坏单例模式

  • 反射破坏单例模式
  • 反序列化破坏单例模式
  • Unsafe破坏单例模式
//测试
public class TestSingleton {
    public static void main(String[] args) {
    	//此时仅调用Singleton1的其他方法,
    	//促使类加载(初始化),调用了构造方法
    	//创建Singleton1对象赋值给INSTANCE,
    	//即使未调用获取该对象的方法,也创建了对象(未使用就创建)
        Singleton1.otherMethod();
        System.out.println("---------------------------");
        //两次调用getInstance方法,获得的同一对象
        System.out.println( Singleton1.getInstance());
        System.out.println( Singleton1.getInstance());
    }
}

在这里插入图片描述

枚举饿汉式

enum Sex{
    MALE,FEMALE;
}
  1. 枚举类可以视为高级的类,其底层实现是由类实现的;
  2. 下方内容可视为其简单实现
  3. 创建了唯二的两个对象对于这个类,当使用这两个变量时,实际调用的是这两个对象
  4. 对于该枚举类来说,该类的对象只有这两个对象
  5. 由此可见,枚举类可以很方便的控制对象个数
final class Sex extends Enum<Sex>{
    public static final Sex MALE;
    public static final Sex FEMALE;
    
    private Sex (String name,int ordinal){
        super(name,ordinal);
    }
    
    static {
        MALE = new Sex("MALE",0);
        FEMALE  = new Sex("FEMALE",1);
    }
}
//枚举饿汉式
public enum Singleton2 {
    INSTANSE;

    private Singleton2(){
        System.out.println("Singleton2");
    }

    public static Singleton2 getInstance(){
        return INSTANSE;
    }

    //打印枚举类对象的Hash码
    public String toString(){
        return getClass().getName()+"@"+Integer.toHexString(hashCode());
    }

    public static void otherMethod(){
        System.out.println("otherMethod");
    }
}
//测试
public class TestSingleton {
    public static void main(String[] args) {
    	//在进行类加载时,即调用构造方法,创建对象给INSTANSE赋值
        Singleton2.otherMethod();
        System.out.println("---------------------------");
        //获得的是同一对象
        System.out.println( Singleton2.getInstance());
        System.out.println( Singleton2.getInstance());
    }
}

在这里插入图片描述

懒汉式

public class Singleton3 implements Serializable {
    //私有构造
    private Singleton3(){
        System.out.println("private  Singleton3()");
    }
    
    //在类加载时,该变量对象不进行创建
    private static Singleton3 INSTANSE = null;
    
    //在需要使用该类时,才创建它的实例对象
    public static synchronized Singleton3 getInstance(){
    	//多线程下,构造会破环单例,
    	//加上synchronized 关键字,对当前静态方法加上锁
    	//要实现的是在首次创建实例对象时,保证创建对象的唯一性
    	//当创建成功,成功赋值后,不同线程对于该方法的访问将不再需要锁
    	//影响性能
        if (INSTANSE==null){
            INSTANSE = new Singleton3();
        }
        return INSTANSE;
    }
    
    public static void otherMethod(){
        System.out.println("otherMethod()");
    }
}

//测试代码
public class TestSingleton {
    public static void main(String[] args) {
        Singleton3.otherMethod();
        System.out.println("---------------------------");
        System.out.println( Singleton3.getInstance());
        System.out.println( Singleton3.getInstance());
    }
}

在这里插入图片描述

DCL懒汉式(双检锁)

public class Singleton4 implements Serializable {
    //私有构造
    private Singleton4(){
        System.out.println("private  Singleton4()");
    }
    
    //volatile  解决共享变量的可见性,有序性
    private static volatile Singleton4 INSTANSE = null;
    
    public static synchronized Singleton4 getInstance(){
    	if(INSTANSE == null){
    		synchronized (Singlenton4.class){
            	if (INSTANSE==null){
            		INSTANSE = new Singleton4();
        		}
        	}
        }
        return INSTANSE;
    }
    
    public static void otherMethod(){
        System.out.println("otherMethod()");
    }
}

volatile
在这里插入图片描述

饿汉式单例实现为什么不用考虑多线程

无论是饿汉式还是懒汉式,对于对象INSTANSE来说,该对象是一个静态对象,在饿汉式单例模式实现时,该对象在类加载时创建,静态成员变量的创建会放进这个类的静态代码块执行,静态代码块的执行处于类生命周期的初始化阶段,由于虚拟机保证其原子且安全的执行,所以,其该对象的创建有虚拟机保证其原子性。

内部类懒汉式

 public class Singleton5 implements Serializable {
 
    private Singleton5(){
        System.out.println("private Singleton5");
    }
    
    //静态内部类
    private static class Holder{
        //在Holder类加载时,该静态变量将会被创建,
        //由于其为静态成员变量,创建会被放进静态代码块,有虚拟机保证其原子性
        static Singleton5 INSTANSE = new Singleton5();
    }    public static Singleton5 getInstance(){
        //在调用该方法时,才加载内部类Holder,才会创建INSTANSE对象,懒汉式
        return Holder.INSTANSE;
    }
    
    public static void otherMethod(){
        System.out.println("otherMethod");
    }
}
```
### 单例模式在jdk中的体现
* Runtime类:饿汉式单例
* Systerm类中 类型为Console的成员变量 :双检索懒汉式单例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wilihelmi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值