- 序列化和反序列化
- == 与equals方法有什么区别?
- final在java中有什么作用?
- Object类有哪些方法
- 事务四大特性:ACID
- 事务的隔离级别
- 数据库的索引原理
- Java实现多线程的四种方式
- hashmap(concurrenthashmap)
- 基本数据类型、装箱拆箱
- Thread.sleep VS Object.wait()
- wait()、notify()、notifyAll()
- 阻塞和等待的区别
- 除了new还有哪些方式创建对象
- 单例模式
- 工厂模式
- 动态代理
- Spring(IOC)
- 编程题
牛客面经:
- 序列化:将java对象转化为字节序列的过程。
反序列化:将字节序列转化为java对象的过程。
- == 与equals方法有什么区别?
== 操作符专门用来比较两个变量的值是否相等,也就是用来比较变量所对应的内存中所存储的数值是否相同。
如果要比较两个基本类型的数据或者两个变量是否相等,只能用== 操作符
equals方法是用来比较两个独立对象的内容是否相同?
在实际开发中,我们经常使用比较传递进来的字符串内容是否相等。
注意:字符串的比较基本上都是使用equals方法。
总结:是用于比较两个变量的值,equals 是用于比较两个对象内容的
基本类型的数据比较用,引用类型的数据的比较用equals
- final在java中有什么作用?
final修饰的类叫最终类,该类不能被继承。
final修饰的方法不能被重写。
final修饰的变量叫常量,常量必须初始化,初始化之后就不能被修改。
总结:Java关键字,终态修饰符,类不可继承,属性不可修改,方法不可重写
- Object类有哪些方法
Object是所有类的父类,任何类都默认继承Object。Object类到底实现了哪些方法?
1.clone方法
保护方法,实现对象的浅拷贝,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
2.getClass方法
final方法,获得运行时类型。
3.toString方法
该方法用得比较多,一般子类都有覆盖。
4.finalize方法
该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。
5.equals方法
该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。
6.hashCode方法
该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。
7.wait方法
wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
(1)其他线程调用了该对象的notify方法。
(2)其他线程调用了该对象的notifyAll方法。
(3)其他线程调用了interrupt中断该线程。
(4)时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
8.notify方法
该方法唤醒在该对象上等待的某个线程。
9.notifyAll方法
该方法唤醒在该对象上等待的所有线程。
- 事务四大特性:ACID
- 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。无论是操作系统崩溃,还是计算机停止运行,这项要求都要成立。
- 一致性(Consistency)事务作为一个原子性操作,它从一个一致性的数据库状态开始运行,事务结束时,数据库的状态必须再次是一致的。
- 隔离性(Isolation)尽管多个事务可能并发执行,但系统保证,对于任何一对事务Ti和Tj ,在Ti看来, Tj要么在Ti开始之间已经完成,要么在Ti完成之后才开始执行。因此,每个事务都感觉不到系统中有其他事务在并发地执行。
- 持久性(Durability)一个事务成功完成后,它对数据库的改变必须是永久的,即使出现系统故障。
ACID 可以说是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是我们的目的。
- 事务的隔离级别
-
读未提交(read uncommitted)
允许读取未提交的数据。 -
读已提交(read committed)
只允许读取已提交数据,但不要求可重复读。比如,在事务两次读取一个数据项期
间,另一个事务更新了该数据项并提交。 -
可重复读(repeatable read)
只允许读取已提交数据,而且在一个事务两次读取一个数据项期间,其他事务不得更新该数据。但该事务不要求与其他事务可串行化。比如,在两次统计查询中,另一个事务可以插入一些记录,当这些记录中有符合查询条件的,那么就会产生幻读。 -
可串行化(serializable)
看起来事务就好像是串行执行的一样。一个事务结束后,另一个事务才开始执行。
隔离性依次增高。
read uncommitted read committed repeatable read serializable
- 数据库的索引原理
能够快速查找的数据结构都可以作为索引。
哪些数据结构可以做索引?
有序数组、哈希表(hash索引)、平衡二叉树、B树、B+树。
有序数组:查找快、增删慢
哈希表:增加、删除,等值查询都很快。但是区间查找,排序等操作会很慢。(因为哈希表中的元素都是无序的)
平衡二叉树(BST):不管是,增加,删除,还是等值查找,时间复杂度都是O(logn),n 是数据页的数目。
并且支持范围查找(BST有序)。
但是当数据量比较大,页的数目很多时,二叉树的高度会比较高。IO 的次数会比较多。
查找效率低。
B树
B+树
B+树索引又可以分为聚簇索引和辅助索引。它们之间的不同在于,聚集索引的叶子节点存储的是一整行记录的信息,而辅助索引的叶子节点只存放部分信息 (关键字和主键)。
联合索引:联合索引是指对表的多个列进行索引
最左前缀法则。
覆盖索引:InnoDB 存储引擎支持覆盖索引 (covering index), 即从辅助索引中就可以得到要查询的信息,而不需要回表。
- Java实现多线程的四种方式
-
方式一:继承Thread类的方式
创建一个继承于Thread类的子类
重写Thread类中的run():将此线程要执行的操作声明在run()
创建Thread的子类的对象
调用此对象的start():①启动线程 ②调用当前线程的run()方法 -
方式二:实现Runnable接口的方式
创建一个实现Runnable接口的类
实现Runnable接口中的抽象方法:run():将创建的线程要执行的操作声明在此方法中
创建Runnable接口实现类的对象
将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
调用Thread类中的start():① 启动线程 ② 调用线程的run() —>调用Runnable接口实现类的run()
以下两种方式是jdk1.5新增的! -
方式三:实现Callable接口
与使用Runnable相比, Callable功能更强大些
实现的call()方法相比run()方法,可以返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
Future接口可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
FutureTask是Futrue接口的唯一的实现类
FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值 -
方式四:使用线程池
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
- hashmap(concurrenthashmap)
Map接口概述:
将键映射到值的对象。(我们可以根据键快速地查找到值)
Map 中键是唯一的 (不能包含重复的键)
每个键最多只能映射到一个值。
如果所有的键都是小整数,我们可以用一个数组来实现的符号表,将键作为数组的索引,而数组中对应的位置存储键关联的值。
哈希表的核心算法可以分为两步。
用哈希函数将键转换为数组中的一个索引。理想情况下不同的键都能转换成不同的索引值。当然这只是理想情况下,所以我们需要处理两个或者多个键都散列到相同索引值的情况 (哈希碰撞)。
处理碰撞冲突。
a. 开放地址法
线性探测法, 平方探测法, 再散列法…
b. 拉链法
HashMap概述:
基于哈希表的Map接口实现。
允许null键和null值。
不保证映射的顺序,特别是它不保证该顺序恒久不变。
不同步。
HashMap VS Hashtable
相同点:
底层的数据结构都是哈希表
不同点:
a. HashMap是不同步的, Hashtable是同步的
b. HashMap可以允许null键和null值,Hashtable不允许null键和null值
LinkedHashMap概述:
HashMap的子类
Map 接口的哈希表和链表实现,具有可预知的迭代顺序.
链表定义了迭代顺序,该迭代顺序就是键值对的插入顺序。
不同步。
TreeMap概述:
底层的数据结构是红黑树。
如果创建对象时,没有传入 Comparator 对象,键将按自然顺序进行排序。
如果创建对象时,传入了 Comparator 对象,键将按 Comparator 进行排序。
不同步。
Java 给我们提供了哪些并发安全的 Map?我们应该选择哪一个?
-
Hashtable (看一下hashtable源码会发现hashtable有很多方法用的synchronized nchronized 方法,这样的话锁对象就是this,我们知道哈希表table的每个index下挂着一个链表,假如我在index=1的链表下进行增删查操作是不会影响index=2下的数据的,但由于hashtable的锁对象是this,所以,它不允许我们进行以上操作,尽管以上操作并不会发生线程安全,所以,hashtable的并发度是很低的 )
-
Collections.synchronizedMap(Map map) (Collections是一个工具类,并发度和hashtable一样)
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
返回指定有序映射支持的同步(线程安全的)有序映射。 -
ConcurrentHashMap(用它)
Hashtable 锁对象是 this对象
Collections.synchronizedMap(Map map) 锁对象是 mutex
JDK8 ConcurrentHashMap 锁对象是链表的头结点。 大大提高并发度。这样的话,关于起那么的index=1和index=2上两条链表的操作就可以同时进行了。
- 基本数据类型、装箱拆箱
- 装箱 把一个基本数据类型的值 -> 封装到了其包装类的对象
对基本类型的数据,创建一个包装类对象,并且将其对应的基本数据类型的值,封装到该对象中
把自动创建的这个Integer对象的引用,指向这个包装类对象
在绝大部分场景下,基本数据类型的变量都和其对应的包装类对象,可以自动相互赋值(自动装箱和拆箱)
- Thread.sleep VS Object.wait()
相同点是两者都可以使一个线程处于阻塞状态
- 所属不同:
a. sleep定义在Thread类,静态方法
b. wait定义在 Object类中,非静态方法 - 唤醒条件不同
a. sleep: 休眠时间到
b. wait: 在其他线程中,在同一个锁对象上,调用了notify或notifyAll方法 - 使用条件不同:
a. sleep 没有任何前提条件
b. wait(), 必须当前线程,持有锁对象,锁对象上调用wait() - 休眠时,对锁对象的持有,不同:(最最核心的区别)
a. 线程因为Sleep方法而处于阻塞状态的时候,在阻塞的时候不会放弃对锁的持有
b. 但是wait()方法,会在阻塞的时候,放弃锁对象持有
- wait()、notify()、notifyAll()
阻塞自己
wait() (在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前)导致当前线程等待/阻塞
阻塞功能:
在哪个线程中调用了 对象a.wait(),该方法导致调用wait方法的线程处于阻塞状态
唤醒条件:
在 其他线程 调用 此对象的 notify() 方法或 notifyAll()
即因为哪个对象的wait()而处于阻塞状态,要唤醒该线程,必须在同一个对象上调用其notify() 或者 notifyAll()
a.wait() 其他线程 a.notify() 或者notifyAll方法
前提条件:
当前线程 必须 拥有此对象监视器 (当前线程必须持有这个锁对象)
即,当前线程如果要想正确运行wait(),前提条件是 必须在当前线程所持有的 锁对象.wait() 。
说白了,你想用锁对象.wait(),首先得写在synchronized (o) { }中。
wait() 方法除了使线程阻塞之外它还做了什么:
该线程 发布对此监视器(监视器即锁对象)的所有权 (当前线程一旦调用了wait()方法,当前线程释放锁) 并 等待
即阻塞当前线程
直到其他线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。
然后该线程将等到重新获得对监视器的所有权后才能继续执行。就是说唤醒你之后你得取争抢锁对象,才能继续往下执行
通知别人
notify()
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的
notifyAll()
唤醒在此对象监视器上等待的所有线程
- 阻塞和等待的区别:
BLOCKED
一个线程因为等待临界区的锁被阻塞产生的状态
Lock 或者synchronize 关键字产生的状态
进入 blocked 状态是被动的。
WAITING
一个线程进入了锁,但是需要等待其他线程执行某些操作。时间不确定
当wait,join,park方法调用时,进入waiting状态。前提是这个线程已经拥有锁了。
进入 waiting 状态是线程主动的。
- 除了new还有哪些方式创建对象
- Class对象的newInstance()方法
还是上面的Test对象,首先我们通过Class.forName()动态的加载类的Class对象,然后通过newInstance()方法获得Test类的对象
public static void main(String[] args) throws Exception {
String className = "org.b3log.solo.util.Test";
Class clasz = Class.forName(className);
Test t = (Test) clasz.newInstance();
}
- 构造函数对象的newInstance()方法
public static void main(String[] args) throws Exception {
Constructor<Test> constructor;
try {
constructor = Test.class.getConstructor();
Test t = constructor.newInstance();
} catch (InstantiationException |
IllegalAccessException |
IllegalArgumentException |
InvocationTargetException |
NoSuchMethodException |
SecurityException e) {
e.printStackTrace();
}
}
- Object对象的clone()方法
Object对象中存在clone方法,它的作用是创建一个对象的副本。看下面的例子,这里我们依然使用第一个例子的Test类
public static void main(String[] args) throws Exception {
Test t1 = new Test("张三");
Test t2 = (Test) t1.clone();
System.out.println(t2.getName());
}
- 对象反序列化
使用反序列化来获得类的对象,那么这里必然要用到序列化Serializable接口,所以这里我们将第一个例子中的Test作出一点改变,那就是实现序列化接口。
public class Test implements Serializable{
private String name;
public Test() {
}
public Test(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) throws Exception {
String filePath = "sample.txt";
Test t1 = new Test("张三");
try {
FileOutputStream fileOutputStream =
new FileOutputStream(filePath);
ObjectOutputStream outputStream =
new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(t1);
outputStream.flush();
outputStream.close();
FileInputStream fileInputStream =
new FileInputStream(filePath);
ObjectInputStream inputStream =
new ObjectInputStream(fileInputStream);
Test t2 = (Test) inputStream.readObject();
inputStream.close();
System.out.println(t2.getName());
} catch (Exception ee) {
ee.printStackTrace();
}
}
}
- 单例模式
-
懒加载
-
立即加载
public class Singleton {
private Singleton();
static Singleton st = new Singleton();
public static Singleton getInstance(){
return st;
}
}
- 工厂模式
- 动态代理
jdk动态代理:
package com.cskaoyan;
import com.cskaoyan.service.HelloService;
import com.cskaoyan.service.HelloServiceImpl;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 动态代理
*/
public class ProxyTest {
/**
* 没有使用动态代理
*/
@Test
public void mytest() {
//获得委托类对象
HelloService helloService = new HelloServiceImpl();
System.out.println("起床");
helloService.sayHello();
System.out.println("编程");
}
/**
* 使用jdk动态代理
*/
@Test
public void mytest2() {
//在hello world之前 起床
//hello world之后 编程
//先获得委托类对象的实例
HelloService helloService = new HelloServiceImpl();
//去获得一个代理对象来完成增强 → jdk动态代理获得增强对象(代理对象)
//classloader都是和委托类对象相关的
//interfaces都是和委托类对象相关的
HelloService helloServiceProxy = (HelloService) Proxy.newProxyInstance(HelloServiceImpl.class.getClassLoader(),
helloService.getClass().getInterfaces(), new InvocationHandler() {
//invoke中
//1.是要去执行委托类的方法
//2.可以去做增强
//返回值:Object 对应委托类方法执行的返回值
//参数:
// proxy :代理对象
// method: 委托类方法的method
// args: 委托类方法的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("起床");
//invoke方法中可以使用反射来执行委托类的方法
//第一个参数是委托类对象,而不是代理对象
Object invoke = method.invoke(helloService, args);
System.out.println("编程");
return invoke;
}
});
//helloServiceProxy.sayHello(); //使用代理对象去执行才会增强
//helloService.sayHello(); //使用委托对象只会输出hello world
helloServiceProxy.method2();
}
}
- Spring(IOC)
IOC用于实例化类对象,管理类对象
IOC:Inverse of Controll 控制反转
控制:实例的生成权
反转:由应用程序反转给spring
容器:容器是放置实例对象的地方 → Spring容器、IOC容器
原先实例我们想用的时候自己new出来(主动的过程);到了Spring阶段,把实例的生成权交给了Spring容器,由Spring容器进行生成并管理,当我们想用的时候就从容器中取出来。
DI:Dependency Injection 依赖注入
依赖:
谁依赖谁? 应用程序依赖Spring容器
为什么依赖? Spring容器包含了应用程序必须的内容
注入:
谁注入谁? Spring容器注入给应用程序
注入了什么?应用程序运行所必须的资源
AOP:面向切面编程
作用:给容器中的组件做增强
之前做增强是通过oop(面向对象)编程,继承父类(静态代理)或传入委托类对象(动态代理)。
之前是使用动态代理给一个类生成代理对象
aop是给容器中的组件批量生成代理对象 → 把要增强的方法放到一起 → 这个范围称为切面。aop是一个范围更广的增强
- synchorized和volatile区别
- 四道编程题
- 快速排序实现(手写代码)
public class 快速排序 {
public static void main(String[] args) {
int[] nums = new int[]{1, 9, 5, 4, 2, 7, 0, 2, 6, 8};
QuickSort(nums, 0, nums.length - 1);
System.out.println(Arrays.toString(nums));
}
public static void QuickSort(int nums[], int low, int high) {
int temp;
int i = low, j = high;
if (low < high) {
temp = nums[low];
while (i < j) {
while (i < j && nums[j] >= temp) j--;
if (i < j) {
nums[i++] = nums[j];
}
while (i < j && nums[i] < temp) i++;
if (i < j) {
nums[j--] = nums[i];
}
}
nums[i] = temp;
QuickSort(nums, low, i - 1);
QuickSort(nums, i + 1, high);
}
}
}
- 合并两个链表(手写代码)
public ListNode mergeTwoLists(ListNode h1, ListNode h2) {
ListNode head = new ListNode(-1);
ListNode h = head;
while(h1 != null && h2 != null){
if(h1.val < h2.val){
head.next = h1;
h1 = h1.next;
}else{
head.next = h2;
h2 = h2.next;
}
head = head.next;
}
if(h1 != null) head.next = h1;
if(h2 != null) head.next = h2;
return h.next;
}
- 二路归并排序
package com.leetcode;
import java.util.Arrays;
/**
* @author liushihao <liushihao@kuaishou.com>
* Created on 2021/2/25 10:32 下午
*/
public class MergeSort {
public static void main(String[] args) {
int[] nums = new int[]{1, 9, 5, 4, 2, 7, 0, 2, 6, 8};
mergeSort(nums, 0, nums.length - 1);
System.out.println(Arrays.toString(nums));
}
//二路归并排序
public static void mergeSort(int nums[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
mergeSort(nums, low, mid);
mergeSort(nums, mid + 1, high);
merge(nums, low, mid, high);
}
}
public static void merge(int[] a, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;
int j = mid + 1;
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (a[i] < a[j]) {
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = a[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = a[j++];
}
// 把新数组中的数覆盖nums数组
for (int x = 0; x < temp.length; x++) {
a[x + low] = temp[x];
}
}
}
- 树的最大距离