二分查找
目标
- 掌握手写二分查找的代码及细节
- 快速解答二分查找的选择题
编写二分查找的代码
- 前提:有已排好序的数组A
- 定义左边界L,右边界R,确定搜索范围,循环执行二分查找(3,4两步)
- 获取中间索引值M=Floor((L+R)/2) <向下取整>
- 中间索引的值A[M]与待搜索的值T进行比较
- A[M]==T 表示找到,返回中间索引
- A[M]>T ,中间值右侧的其他元素都大于T,无需比较,中间索引左边去找,M-1设置为右边界,重新查找
- A[M]<T ,中间值右侧的其他元素都小于T,无需比较,中间索引右边去找,M+1设置为左边界,重新查找
- 当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,5,8,11,19,22,31,35,40,45,48,49,50 当二分查找值为48的节点时,查找成功需要比较的次数
4次
- 有一个有序表1,5,8,11,19,22,31,35,40,45,48,49,50 当二分查找值为48的节点时,查找成功需要比较的次数
-
- 使用二分法在序列 1,4,6,7,15,33,39,50,64,78,75,81,89,96中查找元素81时,需要经过( 4 )次的比较
-
- 在已经排序的128个数组中查找一个数,需要比较次数最多不超过多少次
7次
- 在已经排序的128个数组中查找一个数,需要比较次数最多不超过多少次
-
奇数二分取中间
-
偶数二分取中间靠左
-
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;
}
}
文字描述(以升序为例)
- 一次比较数组中相邻的两个元素大小,若a[j]>a[j+1] ,则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后
- 重复以上步骤,直到整个数组有序
优化方式
每轮冒泡时,最后一次交换索引可以作为下一轮冒泡的比较次数,如果这个值为零,表示整个数组有序,直接退出外层循环即可
选择排序
代码实现
/**
* @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;
}
}
}
}
文字描述
- 将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选取最小的元素,放入排序的子集
- 重复以上步骤,知道整个数组有序
与冒泡排序比较
- 二者平均时间复杂度都是O( n 2 n^2 n2)
- 选择排序一般要快于冒泡,因为其交换次数少
- 但如果集合有序度高,冒泡优先选择
- 冒泡是属于稳定排序的算法,而选择是属于不稳定的排序
插入排序
代码实现
/**
* @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));
}
}
}
文字描述
- 将数组分为两个区域,排序区和未排序区域,每一轮从未排序区域中选取出第一个元素,插入到排序区域(需要保证顺序)
- 重复以上步骤,直到整个数组有序
优化方式
- 待插入元素进行比较时,遇到比自己小的元素,就代表找到了插入位置,无需进行后续比较
- 插入时可以直接移动元素,而不是交换元素
与选择排序比较
- 两者平均复杂度都是O( n 2 n^2 n2)
- 大部分情况,插入排序略优于选择排序
- 有序集合插入排序的时间复杂度为O(n)
- 插入排序属于稳定算法,而选择排序不属于稳定排序
插入和选择
分别使用插入排序和选择排序算法,对序列18,23,19,9,23,15进行排列。第三趟排序后的结果为
- 选择排序
- 9,23,19,18,23,15(选择最小元素9与下标为0的元素交换)
- 9,15,19,18,23,23
- 9,15,18,19,23,23
- 插入排序
- 18,23,19,9,23,15 (下标为1的元素值,经过判断在原位置不变化)
- 18,19,23,9,23,15
- 9,18,19,23,23,15
快速排序
- 每一轮排序选择一个基准点(pivot)进行分区
- 让小于基准点的元素进入一个分区,大于基准点的进入另一个分区
- 当分区完成时,基准点元素的位置就是其最终的位置
- 在子分区内重复以上过程,直至子分区元素个数少于等于1,这体现了分而治之的思想
实现方式
- 单边循环快排
1. 选择最右边的元素作为基准点元素
2. j 指针负责找到比基准小的元素,一旦找到则与 i 进行交换
3. i 指针维护小于基准点元素的边界,也是每次进行交换大的目标索引
4. 最后基准点与i交换,i即为分区位置 - 双边循环快排
- 选择最做元素作为基准点元素
- j指针负责从右向左找比及转电销的元素,i指针负责从左向右找比基准点大的元素,一旦找到两者交换,直至i,j相交
- 最后基准点与此时的i交换,i即为分区位置
- 双边循环要点
- 基准点在左边,并且要先移动j再移动i
- while(i<j){//当i>j时,退出循环
//j 从右找小的元素
while(i<j&&a[j]>pv){//当j小于等于基准点时退出(同时判断i<j是防止在判断过程中违反外部循环)
j–;
} - 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;
}
}
特点
- 平均时间复杂度是O(n$\log 2 2 2$ n ),最坏时间复杂度O( n 2 n^2 n2)
- 数量比较大时,优势十分明显
- 属于不稳定排序
集合
ArrayList
- 目标
- 掌握ArrayList 的扩容机制
- 掌握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:- 原集合没有元素,在调用add时,数组扩容长度为10
- 原集合有元素,在调用add时,扩容为原数组的1.5倍
- addAll(Collectionc)没有元素时,扩容为Math.max(10,实际元素个数)【选择较大的数】,有元素时为Math.max(原容量的1.5倍,实际元素个数)
eg:- 原集合没有元素,在调用addAll时【选择:添加的元素个数,10 中的较大值】
- 添加的元素个数若大于10,则使用实际元素个数作为扩容数组长度;
- 添加的元素个数若小于10,则使用10作为扩容数组长度
- 原集合有元素,在调用addAll时【选择:添加的元素个数加上原元素个数和,原元素个数的1.5倍 中的较大值】
- 添加的元素个数加上原元素个数若大于原元素个数的1.5倍,则使用添加的元素个数加上原元素个数作为扩容数组长度;
- 添加的元素个数加上原元素个数若小于原元素个数的1.5倍,则使用添原元素个数的1.5倍作为扩容数组长度;
- 原集合没有元素,在调用addAll时【选择:添加的元素个数,10 中的较大值】
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
- 基于数组,需要连续存储
- 随机访问快(根据下标进行访问)
- 尾部插入,删除性能可以,其他部分插入删除都会移动数据,因为性能会低
- 可以利用cpu缓存,局部性原理
- LinkedList
- 基于双向链表,无需连续内存
- 随机访问慢(要沿着链表遍历)
- 头尾插入删除性能高
- 占用内存多
- ArrayList头部插入较慢,尾部插入快,LinkedList头尾插入较快,中间插入较慢(将指针移至中间位置较为耗费时间)
HashMap
底层数据结构,1.7和1.8有何不同?
1.7 数组+链表
1.8 数组+(链表|红黑树)
为什么要用红黑树呢,为何一上来不树化,树化阈值为何是8,何时会树化,何时会退化为链表?
-
- 只用数组+链表,当链表过长时,会影响查找性能;
- 红黑树用来避免DoS攻击,防止链表超长时影响性能下降,树化应该是偶然情况
-
- 当链表比较短时,其查找性能优于红黑树;只有到链表的长度较长时,红黑树的查找性能才会优于链表;且树占用的内存大于链表;
- hash表的查找,更新的时间复杂度是O(1),而红黑树的查找,更新的时间的时间复杂度是O(log 以2为底的n),TreeNode占用空间也比普通Node的大,如果非必要,尽量还是使用链表
- hash值如果够随机,则在hash表内按柏松分布,在负载因子0.75的情况下,长度超过8的链表出现概率是0.00000006,选择8就是为了让树化几率足够小
- 要满足数组长度大于等于64,且链表长度大于阈值8;
-
- 退化情况1:在扩容时如果拆分树时,分化后的树元素个数<=6则会退化为链表
- 退化情况2:remove树节点前,进行检查,若root,root.left,root.right,root.left.left(树的根节点,根节点的左孩子,右孩子,左孙子)有一个为null,移除该节点,树会退化为链表
存入一对键值对:对其键值求哈希值,原始hash和二次hash,对二次hash值根据数组容量进行求模运算,计算桶下标;实现元素的快速查找。
链表过长的解决----扩容:当添加某键值对,添加到某个桶中,Map中元素个数大于数组容量的0.75倍(加载因子,扩容因子)时,或者某个桶内元素超8时,则进行扩容(数组长度两倍递增);当不同元素的hash值模数组长度获得的桶下标一致但其hash值不同时,当数组长度改变,其不同元素的桶下标会发生变化,故链表的长度可能减短
链表过长的解决----树化:
- 数组容量必须大于64时,才可能会树化,否则优先考虑对数组进行扩容,减短链表长度
- 当数组容量必须大于64且链表元素长度大于树化阈值8时,才会树化
索引如何计算?hashCode都有了,为何还要提供hash()方法?数组容量为何会是2的n次幂?
- 索引由对元素键值进行hashCode运算后,再进行二次hash,即HashMap的hash()方法进行运算,得到的结果对数组容量进行求模运算
key | 原始hash | 二次hash | 桶下标 |
---|---|---|---|
a | 97 | 97 | 1 |
数组初始长度为16
97 % 16 ⇒ 1
97 & (16-1) ⇒1
求模运算底层为除法运算,比较麻烦,可换算为按位与运算(该情况仅适用于数组长度为2的倍数时)
97 % 64 ⇒ 33
97 & (64-1) ⇒ 33
- 二次hash()是为了综合高位数据,让哈希分布更为均匀;hashCode()求下标,只有低位数据参与运算,高位数据的不同不会影响下标的结果,造成更多冲突,则分布不均匀
- 计算索引时,如果是2的n次幂可以使用位与运算代替取模,效率更高;扩容时 hash & oldCap(旧容量)==0的元素留在原来位置,否则更新位置 = 旧位置 + oldCap;
- 上述都是为了配合容量为2的n次幂时的优化手段,例如hashTable的容量就不是2的n次幂,并不能说哪种设计更优,应该是设计者综合了各种因素,最终选择了2的n次幂作为容量(当容量为一个较大的质数时,分布会更加均匀)
介绍一下put方法流程,1.7和1.8有何不同?
- 流程
- HashMap是懒创建,首次使用才创建数组
- 计算索引(桶下标)
- 如果桶下标害没人占用,创建Node占位并返回
- 如果桶下标已被占用
- 已经是TreeNode走红黑树的添加或更新逻辑
- 是普通Node,走链表的添加或更新逻辑,如果链表长度超过阈值(且数组长度大于64),走树化逻辑
- 返回前检查容量是否超出阈值,一旦超出进行扩容
- 不同
- 链表插入节点时,1.7是头插法,1.8是尾插法
- 1.7是大于等于阈值没有空位时才扩容,1.8是大于阈值就扩容
- 1.8在扩容计算Node索引时,会优化
加载因子为何默认为0.75?
在空间占用与查询时间之间取得了较好的权衡;大于这个值,空间节省了,但是链表就会比较长影响性能;小于这个值,冲突减少了,但是扩容就会更为频繁,空间占用多。
多线程下操作HashMap会有什么问题?
- 扩容死链(1.7)
- 数据错乱(1.7,1.8)
[eg:不同线程添加元素时,t1和t2两个线程放入map的元素,它们键值hash值一致,在put方法判断该数组位置是否已有节点时,两个线程都进入判断语句,此时均没有节点占用数组空间,同时将节点赋值给数组,则会覆盖元素]
扩容死链
- 扩容元素迁移过程图示
- 多线程扩容死链形成过程图示
死链形成
key能否为null,作为key的对象有什么要求?
- HashMap的key可以为null,但Map的其他实现则不然,否则会出现空指针
- 作为key的对象必须实现hashCode和equals,并且key的内容不能修改(不可变)
String对象的hashCode()如何设计的,为啥每次乘的都是31?
- 所有对象的hasshCode()设计的目的都是为了使散列更加均匀,每个字符串的hashCode都足够独特
- 字符串中的每一个字符都可以表现为一个数字,称为Si,其中i的范围时0~n-1
- 散列公式为:S0 * 31的(n-1)次方+S1* 31的(n-2)次方+···+Si * 31的(n-1-i)次方+···+Sn-1 * 31的0次方
- 31代入公式有比较好的散列特性,并且31 * h可以被优化为(便于计算)
- 32 * h - h
- 2的5次方 * h - h
- h << 5 - h
设计模式
单例模式
目标
- 掌握单例模式常见的五种实现方式
- 了解jdk中哪些地方体现了单例模式
饿汉式
- 构造私有
- 提供私有静态成员变量(该类对象)
- 提供公共的静态方法(返回私有静态对象)
只要类初始化了,该对象就会创建,跟是否使用该对象无关,所以称为饿汉式
//饿汉式
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;
}
- 枚举类可以视为高级的类,其底层实现是由类实现的;
- 下方内容可视为其简单实现
- 创建了唯二的两个对象对于这个类,当使用这两个变量时,实际调用的是这两个对象
- 对于该枚举类来说,该类的对象只有这两个对象
- 由此可见,枚举类可以很方便的控制对象个数
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的成员变量 :双检索懒汉式单例