为了方便阅读JDK的源码,我就把讲解放在代码的注释里边了。
于昨日2019/10/9日,面了一个岗位,面试题我拍下来了,给大家先看下吧,后面我再接着更新答案。可能AV画质了,原图我会共享到群,欢迎大家进群吹水(183579482)。
目录直通车
String、StringBuilder、StringBuffer 的区别是什么?
说一说自己对于 synchronized 关键字的了解,自己在项目里是如何使用的?
统计某段字符串中的某个字符串的个数?
/**
* 统计某段字符串中的某个字符串的个数
* 解题关键:String.indexOf()
*
* @author DJun
*/
public class FindKeyWords {
public static void main(String[] args) {
String text = "An array is a data struct. Trying to find array which is the keyword";
int arrayCount = 0;
int index = -1;
String arrayStr = "array";
index = text.indexOf(arrayStr);
while (index >= 0) {
++arrayCount;
index += arrayStr.length();
index = text.indexOf(arrayStr, index);
}
System.out.println("array数量为" + arrayCount);
}
}
随机获取100以内的10个数且不重复并排序
/**
* 随机获取100以内的10个数且不重复并排序
* <p>
* 解题关键:Set 的特性有无序性、不可重复性。无序性不等于随机性,位置是按照哈希值来定的。
* 要求添加进Set中的元素所在的类,一定要重写equals()和 hashCode()方法,进而保持Set的不可重复性。
*
* @author DJun
*/
public class NoRepeatRandom {
public static void main(String[] args) {
Random random = new Random();
HashSet<Integer> numSet = new HashSet<>();
List<Integer> list = new ArrayList<>();
while (numSet.size() < 10) {
int num = random.nextInt(100);
numSet.add(num);
}
// 1、方法一
TreeSet<Integer> ts = new TreeSet(numSet);
/* 这边非常感谢id名为 _易 的朋友指出: TreeSet的构造函数里面调用了addAll()方法,而addAll()方法会进行排序。
我翻阅的一下TreeSet的源码
调用的构造函数为
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
addAll() 调用了 SortedSet<? extends E> set = (SortedSet<? extends E>) c;实现了排序
*/
// ts.comparator();
System.out.println(ts);
// 2、方法二
Iterator iterator = numSet.iterator();
while (iterator.hasNext()) {
list.add((Integer) iterator.next());
}
Collections.sort(list);
System.out.println(list);
}
}
统计某段字符串中的某个字符串的个数
/**
* 统计某段字符串中的某个字符串的个数
* 解题关键:String.indexOf()
*
* @author DJun
*/
public class FindKeyWords {
public static void main(String[] args) {
String text = "An array is a data struct. Trying to find array which is the keyword";
int arrayCount = 0;
int index = -1;
String arrayStr = "array";
index = text.indexOf(arrayStr);
while (index >= 0) {
++arrayCount;
index += arrayStr.length();
index = text.indexOf(arrayStr, index);
}
System.out.println("array数量为" + arrayCount);
}
}
HashMap 底层原理
/**
* HashMap 底层原理 面试题
*
* 1、为什么用HashMap?
* (1)在JDK1.8之前,HashMap 采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改。
* (2)相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,
* 将链表转化为红黑树,以减少搜索时间。TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。
* 红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
*
* HashMap是非synchronized,所以HashMap很快
* HashMap可以接受null的KV,而Hashtable则不能(原因就是equlas()方法需要对象,因为HashMap是后出的API经过处理才可以)
* 2、HashMap源码分析
* 先说HashMap的四个构造函数
* (1) HashMap()
* 构造一个空的HashMap,初始容量为16,负载因子为0.75
* 平时用的最多的Map集合就是HashMap,速度快但是线程不安全,所以公司喜欢考求职者
* (2) HashMap(int initialCapacity)
* 构造一个初始容量为initialCapacity,负载因子为0.75的空的HashMap,这个构造方法调用了(3),所以关键点还是在下面。
*
* (3) HashMap(int initialCapacity, float loadFactor)
* a.构造一个空的初始容量为initialCapacity,负载因子为loadFactor的HashMap。
* b.当指定的初始容量 < 0 时抛出IllegalArgumentException异常,当指定的初始容量 > MAXIMUM_CAPACITY时,
* 就让初始容量 = MAXIMUM_CAPACITY。当负载因子小于0或者不是数字时,抛出IllegalArgumentException异常
* 设定threshold。 这个threshold = capacity * load factor 。当HashMap的size到了threshold时,就要进行resize,也就是扩容。
* tableSizeFor()的主要功能是返回一个比给定整数大且最接近的2的幂次方整数,如给定10,返回2的4次方16.
*
* HashMap 的长度为什么是 2 的幂次方?
* 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的
* 范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应
* 用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之
* 前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算
* 方法是“ (n - 1) & hash ”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
*
*
* 3、HashMap的使用:
* (1) KV是否能够重复的原因:
* Key使用Set来存放的,所以不能重复;Value是用Collections来存放,所以可以重复。
* (2)遍历方法
* 既然key是用Set来存放的,那么可以用 map.keySet 和 map.entrySet 来获取
* keySet:获取key集,在 map.get(key) 获取value,也可用遍历Collection的方法来获取Value,
* 比如迭代器:先利用Collection的特性获取到Value集,Iterator iterator = map.values().iterator().
* entrySet:同时获取 key 集和 Value 集,再用循环遍历取出一对一对的kv。
*
*
* 4、HashMap 与 Hashtable的区别:
*
* 下面这个方法是 Hashtable 线程安全 与 KV不能为 null 的原因(如果 equals 中的值为null,直接抛空指针异常):
*
* public synchronized boolean contains(Object value) {
* if (value == null) {
* throw new NullPointerException();
* }
*
* Entry<?,?> tab[] = table;
* for (int i = tab.length ; i-- > 0 ;) {
* for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
* if (e.value.equals(value)) {
* return true;
* }
* }
* }
* return false;
* }
*
* (1) 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
* synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
* (2) 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在
* 代码中使用它;
* (3) 对 Null key 和 Null value 的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键
* 所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
* (4) 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,
* 之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来
* 的 2 倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充
* 为 2 的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总 是使
* 用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
* (5) 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,
* 将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
*
* 5、JDK1.8之前的 HashMap 原理
* JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经
* 过扰动函数处理过后得到 hash 值,然后通过 - 判断当前元素存放的位置(这里的 n 指的是数组的
* 长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的
* 话,直接覆盖,不相同就通过拉链法解决冲突。
* 所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的
* hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
*
* 对比1.7 和 1.8 的 hash 方法:
*--------------1.7----------
* static int hash(int h) {
* h ^= (h >>> 20) ^ (h >>> 12);
* return h ^ (h >>> 7) ^ (h >>> 4);
* }
*
*--------------1.8----------
* static final int hash(Object key) {
* int h;
* return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
* }
* 显然1.8 简洁了很多。1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
* 所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希
* 冲突,则将冲突的值加到链表中即可。一般不用Hashtable 而用 ConcurrentHashMap.
*
* ConcurrentHashMap 和 Hashtable 的区别?
* ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
* 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟
* HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类
* 似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
* 实现线程安全的方式(重要): ① 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了
* 分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁
* 竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑
* 树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很
* 多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,
* 但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全, 效
* 率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put
* 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
*
* @author DJun
*/
public class HashMapPrinciple {
// 点击以下进去看源码
HashMap hashMap ;
Hashtable hashtable ;
ConcurrentHashMap concurrentHashMap;
LinkedHashSet linkedHashSet;
}
你怎么理解Java中的异常处理?
/**
* 你怎么理解Java中的异常处理?
* 在 Java 里面异常类最终都继承于 Throwable 类
* Throwable: 有两个重要的子类: Exception(异常) 和 Error(错误)
*
* Error : 程序无法处理的错误,足见比较严重的问题,
* 大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。
*
* Exception :是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。
* RuntimeException 异常由 Java 虚拟机抛出。
*
* @author DJun
* @date 2019/10/4
*/
public class ExceptionPrinciple {
Throwable t ;
}
接口和抽象类的区别是什么?
/**
* 接口和抽象类的区别是什么?
* 1、一个类可以实现多个接口,但却只能实现一个抽象类
* 2、接口默认的方法是public,且不能在接口中实现方法。抽象类却可以有非抽象的方法。
* 3、一个类实现接口的话要实现接口类中的所有方法,而抽象类不一定。
* 4、接口不能用 new 来实例化,但可以声明。
* 5、接口中的实例变量默认是 final 类型的,而抽象类中则不一定。
*
* @author DJun
* @date 2019/10/4
*/
public class InterfaceAndAbstract {
}
线程相关
/**
* 1、最基本的创建线程的两种方式:继承Thread 、实现 Runnable 接口。还有一种就是线程池。
* 2、这两者的区别是什么?
* (1)如果多个线程需要处理同一份资源,那么就用 runnable,因为在
* Java里面,类是单继承的,用这种实现接口的方式恰好可以解决单继承的
* 局限性。最终还是需要把实现了Runnable 接口类的对象传入Thread,用Thread的对象来操作线程。
*
* (2)继承Thread的类可以直接操作线程,不需要再创建一个Thread的对象。
*
* 3、死锁
* 死锁是多线程操作同一块资源时,都在等待对方执行完毕而陷入无限等待的状态。
*
* 4、乐观锁、悲观锁(一般数据库使用)
* (1)乐观锁总假设最好的情况,每次拿数据的时候,都认为对方不会修改数据,所以不会上锁,
* 但是在更新这份数据的时候,会判断在此期间有没有别人修改这个数据,一般用版本号和CAS来实现。
* (2)悲观锁反之,第一个人来拿数据的时候,就会上锁,直到执行完成之后,才允许别人来拿数据。
*
* 5、wait 与 sleep()的区别?
* sleep() 来自 Thread类,wait()来自 Object 类,
* 调用 sleep(),不会释放对象锁,不会让出CPU,需要指定睡眠的时间,时间一到才苏醒。
* 调用 wait() 却会释放对象锁,可以让其他线程占用 CPU,通过notify()或notifyAll()来唤醒。
*
* 6、ThreadLocal 和 Synchronized 的区别
* (1)ThreadLocal是线程局部变量,局限于线程内部的变量,也就是说它是属于线程自身所有,不在多个线程间共享。
* Java提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。ThreadLocal 为每一个线程都提供了变量的副本,
* 使得每个线程在某一时间访问到的并不是同一个对象,以此隔离多线程对数据的数据共享。
*
* (2)synchronized 相当于悲观锁,利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。
*
*
* @author DJun
* @date 2019/10/4
*/
public class Multithreading {
// 点击进去看源码
Thread thread;
ThreadLocal threadLocal = new ThreadLocal();
}
String、StringBuilder、StringBuffer 的区别是什么?
/**
* String、StringBuilder、StringBuffer 的区别是什么?
* 1、String 线程安全的原因是:private final char value[],其它两个没有用 final 修饰符,
* 2、StringBuilder 和 StringBuffer 本质上没有区别,
* 只是StringBuilder去掉了线程安全那部分,用于字符串拼接不需要线程安全(用于单线程),速度更快。
* 3、所以 StringBuffer 是线程安全的(用于多线程),因为加了同步锁。
*
* == 与 equals 的区别是什么?
* 都是判断地址是否相同,也就是说判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
* 但 equals 一般有两种使用情况:
* 1、类没有重写 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
* 2、类重写了 equals() 方法。一般,我们都重写 equals() 方法来两个对象的内容相等;若它们的内容相等,
* 则返回 true (即,认为这两个对象相等)。
*
* 说明:
* 1、String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,
* 而 String 的equals 方法比较的是对象的值。
* 2、当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,
* 如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
*
* @author DJun
* @date 2019/10/4
*/
public class StringBuilderAndBuffer {
// 点击进去看源码
String str;
StringBuilder stringBuilder;
StringBuffer stringBuffer;
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b 为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
说一说自己对于 synchronized 关键字的了解,自己在项目里是如何使用的?
/**
* 说一说自己对于 synchronized 关键字的了解?
* Synchronized 解决的是多线程访问资源的同步性,可以给Synchronized 修饰的方法
* 或代码块加锁,保证在任意时刻都只能有一个线程执行。加的这个锁属于重量级锁,效率
* 低下。因为在底层用的监视器锁需要用到操作系统的 Mutex Lock 来实现,Java 的线
* 程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程就需要操作系统来
* 帮忙完成,然而操作系统实现把线程从操作态转换成内核态的期间会需要相对较长的时间,
* 时间成本较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java6
* 之后Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized锁
* 效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、
* 锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
*
* 在项目中是如何使用的?
* synchronized 关键字最主要的三种使用方式:
* 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
* 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作
* 用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态
* 资源,不管 new 了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程 A 调用一个实
* 例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允
* 许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态
* synchronized 方法占用的锁是当前实例对象锁。
* 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方
* 法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和
* synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized 关键字加到非 static 静态
* 方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量
* 池具有缓冲功能!
*
*
*
* @author DJun
* @date 2019/10/5
*/
public class SynchronizedUsing {
private synchronized void method1() throws InterruptedException {
System.out.println("method1 begin at:" + System.currentTimeMillis());
Thread.sleep(6000);
System.out.println("method1 end at:" + System.currentTimeMillis());
}
private synchronized void method2() throws InterruptedException {
while (true) {
System.out.println("method2 running");
Thread.sleep(200);
}
}
private static SynchronizedUsing instance = new SynchronizedUsing();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
instance.method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 1; i < 4; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1 still alive");
}
});
Thread thread2 = new Thread(() -> {
try {
instance.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}
你能手写一下 双重校验锁实现对象单例 的代码吗?
简单通俗的说一下单例里面的 懒汉模式 和 饿汉模式:前者就是懒,在自己的类里面创建一个静态的当前类的对象,外部直接调用改方法即可,不需要在new。后者,饥渴无比,来者都欢迎,但是你来的时候没有对象,则才new 一个,如果有对象了,就不让你进来了。
/**
* 你能手写一下双重校验锁实现对象单例(线程安全)的代码吗?
*
* 注意:在这里使用 volatile 修饰是非常有必要的,解决数据易变失效问题
*
* 通过下面这个实例来说明 volatile 的作用:
* (1)下面这个类是不安全的 因为get和set方法都是在没有同步的情况下进行的。
* 如果线程1调用了set方法,那么正在调用的get的线程2可能会看到更新后的value值,也可能看不到。
* public class MutableInteger {
* private int value;
* public int get(){
* return value;
* }
* public void set(int value){
* this.value = value;
* }
* }
* 将value声明为volatile变量:private volatile int value; 即可解决上面这个问题。
*
* (2)下面这个段代码其实是分为三步执行:
* 1. 为 uniqueInstance 分配内存空间
* 2. 初始化 uniqueInstance
* 3. 将 uniqueInstance 指向分配的内存地址
*
* 但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在
* 多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用
* getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被
* 初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
*
* @author DJun
* @date 2019/10/6
*/
public class VolatileSingleton {
private volatile static VolatileSingleton uniqueInstance;
private VolatileSingleton() {}
public static VolatileSingleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
// 类对象加锁
synchronized (VolatileSingleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new VolatileSingleton();
}
}
}
return uniqueInstance;
}
}
HashSet 是如何保证不重复的
向 HashSet 中 add ()元素时,判断元素是否存在的依据,不仅要比较 hash 值,同时还要结合 equles 方法比较。 HashSet 中的 add ()方法会使用 HashMap 的 add ()方法。以下是 HashSet 部分源码:
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap 的 key 是唯一的,由上面的代码可以看出 HashSet 添加进去的值就是作为 HashMap 的 key。所以不会重复( HashMap 比较 key 是否相等是先比较 hashcode 在比较 equals )。
转载:https://blog.csdn.net/qq_41647999/article/details/102093277
查看评论