目录
1. 类、实例、对象的区别
- 类是一种抽象化的概念,是用来描述对象的一些特性的
- 根据是否有abstract字段标记,可以分为普通类和抽象类
- 普通类产生的对象叫做实例化对象,而抽象类是不能实例化的
2. Java创建对象的几种方式
- 使用new创建对象
- 使用反射的机制创建对象
- 使用构造器创建:Constructor类的newInstance方法
- 采用clone创建
- 使用反序列化创建:Object的readObject方法
3. java中的四种引用类型
1. 强引用
白话文:new一个对象,就是强引用,且不会被垃圾回收器回收,除非把引用指向null,才会被回收。
内存不足会报OOM异常
2. 软引用
白话文:当内存不足时,垃圾回收器会回收软引用。
3. 弱引用
白话文:比软引用还要弱,无论内存是否充足,都会被垃圾回收器回收。
4. 虚引用
白话文:有跟没有一样,通常和引用队列(ReferenceQueue )搭配使用
例如:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知
4. 获取一个类的class对象有哪几种方式?
- 类名.class:相对简单,但是需要明确具体类的静态成员(啊?为什么是静态成员呢,这点体现在哪里,原因是:首先你的类都创建好了,那说明类加载好了,类加载器的初始化阶段,编译器会去初始化这个类信息,包括了静态成员)
- 类的对象名.getClass():本质是用的Object的getClass方法,使用该方法2个条件,必须明确具体的类,且要创建对象
- Class.forName(“类的全限定名”) :该方式只需类的全类名即可,更为方便,扩展性更强。
代码亲测:这三种方式创建的类的方式的hashcode都是一样的
5. Object类中的常用方法(重要)
1. 多线程相关的方法
- notify():唤醒在此对象监视器上等待的单个线程
- notifyAll():唤醒在此对象监视器上等待的所有线程
- wait(long timeout):使当前线程等待,当其他线程调用此对象的notify()方法或notifyAll()方法,或者超过指定的时间后结束等待
- wait(long timeout, int nanos):和wait(long timeout),不过多了纳秒的时间入参
- wait():当前线程等待
2. 其他方法
- registerNatives():私有方法,应该是注册native的
- getClass():返回此Object的运行Class类
- hashCode():用于获取对象的哈希值
- equals(Object obj):用于确认两个对象是否相同
- clone():创建并返回此对象的一个副本
- toString():返回该对象的字符串表示
- finalize():当改对象确定无引用时,由对象的垃圾回收器调用此方法,把对象的内存回收掉
6. IO与NIO的区别
1. IO(Input OutPut)
1. 面向流
面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
2. 阻塞的
这意味着,当一个线程调用**read() 或 write()**时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
白话文:看这样子,IO是单线程操作
2. NIO (New Input OutPut)
关键字:通道、缓冲区、不需要用户态和内核态的切换、直接访问内存
1. 面向缓冲区
Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
白话文:NIO多了一个缓冲区,但也多了很多麻烦(检查自己要的数据在不在缓存区、赛数据的时候不能把未处理的数据给覆盖掉)
2. 非阻塞的(通道和缓冲区实现)
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
白话文:通过通道和缓冲区实现,一个线程发送请求后,就可以做其他事情了,等数据都获取完毕,他们会提醒线程过来读取(有种助理的内味了,诶帮我去做点事情,好了叫我)
1. 题外话:什么是通道Channel?
表示IO源与目标打开的连接。但是Channel不能直接访问数据,需要和缓冲区buffer进行交互
白话文:能够让IO数据源和目标源(比如什么呢?)有连接,通道类似于铁路,缓冲区相当于火车
3. 选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道(卧槽?这不就是redis中的多路I/O复用模型,但是有点不太一样,redis监控的对象是IO,这里监控的对象是通道。。),你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
白话文:有了选择器,NIO就可以用一个线程去监控多个通道(想点谁就点谁。。我有选择的功能)
4. 为什么NIO会这么快?(基于通道、缓存区、堆外内存)
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer(翻译:直接字节缓冲区??) 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
白话文:NIO是可以让native方法使用堆外内存(原使用Native堆的),不需要用户态和内核态的切换、直接访问内存
7. 什么是fail-fast机制?
1. 什么是fail-fast机制(ConcurrentModificationException什么时候会抛出)
日常吐槽:换个问题吧兄dei,你不如问问什么时候会抛出ConcurrentModificationException (并发修改异常),都是一样的问题,这样我还还记住
Java集合的一种错误检测机制
集合的迭代器在调用next()、remove()方法时都会调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。modCount是在每次改变集合数量时会改变的值。
2. 什么时候会触发fail-fast机制
1.遍历集合的同时修改集合(非常常见)
如遍历中有list,remove操作,这中新手程序员经常犯的操作
2. 多个线程同时对集合进行结构做出改变的操作(未加锁)
多线程同时操作ArrayList进行动态扩容,是不是就有可能触发并发修改异常了
1. 题外话:List如何在遍历时删除元素?
1. 使用Iterator方法中的remove操作
其实在阿里巴巴Java开发手册中原话:不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
白话文:
public interface Iterator<E> {
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
2. 倒序遍历
list的size改变并没有对遍历造成影响,且元素的前移也不会对倒序遍历有影响
ArrayList和LinkedList的区别
1. ArrayList
关键字:add操作、ensureExplicitCapacity、grow方法、Array.copyOf
ArrayList内部维护了一个Object数组,无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量,数组容量扩为 10
1. 数据结构
ArrayList是基于动态数组的,实现了动态扩容
调用grow方法,通过位移操作扩容为原来的1.5倍
2. 相关操作(查询、添加和删除)
ArrayList查询快,添加和删除慢
3. 源码
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
}
- 实现了RandomAccess接口,支持快速随机访问
- 实现了Cloneable接口,能被克隆
4. add操作的2个步骤
1. 判断容量是否充足 ensureExplicitCapacity
判断elementData数组容量是否满足需求
1. 扩容机制
- 每次扩容是原来容量的1.5倍,通过移位的方法实现。
- 使用Array.copyOf进行扩容
2. 在elementData对应位置上设置值
5. ArrayList为什么不是线程安全的?
扩容操作未加锁
6. 如何实现同步list?
1. Collections.sychronizeList
将list转换成同步list
2. 直接使用CopyOnWriteArrayList
- 读时不加锁
- 写入时加锁,写入时创建一个新入组将老数组拷贝进入新数组,并将数据加入新数组
2. LinkedList
1. 数据结构
LinkedList是基于链表的(准确来说是双向链表),可以模拟链式队列,链式堆栈等数据结构
2. 相关操作
LinkedList查询慢,添加和删除快
3. 源码
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
}
1. HashMap
1. 问:HashMap为什么不是线程安全的
- 没有锁操作,两个线程操作同一个hashMap会出现线程安全的问题,可能会导致数据丢失。
- resize的时候会出现死锁,以为hash冲突之后采用链地址法解决hash冲突,但是两个线程都进行扩容的时候,链表使用头插法,导致出现循环引用,出现死锁。(1.8之后 链表都是采用尾插法,避免了死循环的问题)
2. 问:HashMap的长度为什么要是2的n次方(保证取余运算能转化成位运算)
HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;
这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1),
只要满足length是2的n次方,这个等式就成立:hash%length == hash&(length-1)
白话文:hash取模算法(其实就是hash取余算法hash%length),hashMao想要变得更快,采用位运算(hash&(length-1))来代替取余运算,为了保证二者之间是等效的,所以HashMap的长度要是2的N次方
1. 题外话:hash是如何减少碰撞的?
为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1(要这个!);
例如长度为9时候,3&(9-1)=0 2&(9-1)=0,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3 2&(8-1)=2,不同位置上,不碰撞;
白话文:hash的size减1之后的二进制码都是11111111…(简称这家伙),这家伙和所有数字与运算一下,不都是它本身嘛,这还怎么碰撞。。
2. 题外话:什么时候会发生hash碰撞?
接着上个题外话,当提供的数字比hash的大小还要大(因为大的位和这家伙的位&运算一下,就是为0了,相当于取余不是本身了((如5/3 余2,2不是本身)),真的取余了),是不是就有可能存在hash碰撞了呀?
白话文:给了两个数(6和2),hash的大小是4(满足2的n次方),根据hash取余算法的位运算方程6&(4-1) = 2、2&(4-1) = 2,糟糕出现了两个2,hash碰撞了。。
String、StringBuffer、StringBuilder的区别
1. String
String类中使用字符数组保存字符串,final修饰的,不可变的,每次赋值都会产生一个新的对象
private final char value[];
2. StringBuffer(线程安全)
StringBuilder是可变的,但append方法中加了synchronized同步锁,所以是线程安全的
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
}
3. StringBuilder(线程不安全)
StringBuilder是可变的,但append方法中没有加同步锁,所以是非线程安全的
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
try catch finally和return相关的问题
- finally里面的方法时一定会被执行的
- 如果try里有return,执行顺序:先执行try的代码(如果return后面跟的是一个操作,也会先执行),然后再去执行finally方法(如果finally里有return,直接返回),finally执行完之后,再执行try的return操作
- catch里面的如果又return,和第2点流程一样
OOM有哪些情况,SOF有哪些情况
1. OOM(Out Of Memory)
1. 原因
内存溢出、内存泄漏、GC效率低
2. 常见场景
- 内存空间不足,申请不到足够的内存空间
- 垃圾收集器回收垃圾效率太低,也会OOM
- 运行期常量池具备动态扩展性,当intern方法用太多的时候,也会OOM
- 类创建太多的时候,编译期导致方法区内存溢出
- ThreadLocal内存泄漏
2. SOF(StackOverFlowError)
日常吐槽:好家伙,这个栈溢出错误英文这么难记,什么 栈+结束+流+错误,哈哈什么鬼。。
1. 常见场景
- 递归调用
红黑树有哪些特征
红黑树是一颗近似平衡二叉树(会左旋右旋来保持平衡)
- 根节点都是黑色的
- 父子节点不可能同时为红色
- 叶子节点是黑色的空节点(null),不存数据
- 从任意节点到其所有叶子节点的路径都包含相同的黑色节点(用变色或旋转(左旋或右旋)的调整方式实现)
泛型
1. 泛型的作用
泛型可以消除代码中的强制类型转换,同时获得一个附加的类型检查层,该检查层可以防止有人将错误类型的键或值保存在集合中。这就是泛型所做的工作。