1. 基础
Java语言特点:面向对象、通过JVM实现平台无关性和移植性、多线程、网络编程
基本数据类型:int(32)、char(16)、byte(8)、short(16)、long(64)、float(32)、double(64)、Boolean(1)
String是不可变的,用final声明,不可继承,线程安全,字符串常量池
-
String、StringBuffer、StringBuilder
- 可变性:String不可变,是通过new一个字符串来拼接。StringBuffer和StringBuilder可通过append()方法拼接字符串
- 线程安全:String线程安全,StringBuffer的append方法用了synchronized修饰,是线程安全,StringBuilder的append方法没有
- 性能:StringBuilder > StringBuffer > String
深拷贝:拷贝对象和原始对象引用不同,修改原始对象的值不会影响拷贝对象的值
浅拷贝:拷贝对象和原始对象引用相同,修改原始对象的值会影响拷贝对象的值
例子:公会乱斗保存的公会成员,要断开引用-
equals和==的区别
- 基本数据类型,==比较的是值。引用数据类型,==比较的是内存地址
- 基本数据类型没有equals方法。引用数据类型,equals默认比较内存地址,如果有重写则按照重写逻辑
-
访问权限修饰符
- public:对所有类可见,使用对象:类、接口、方法、变量
- protected:对同一个包的类和所有子类可见,使用对象:变量、方法,不能修饰外部类
- private:仅同一个类可见,使用对象:变量、方法,不能修饰外部类
- default:同一包可见,使用对象:类、接口、变量、方法
-
static关键字
- static变量:静态变量,可通过类.变量名访问。内存中只有一个副本,在类加载时被初始化(用来定义事件、协议)
- static方法:静态方法,在static方法内不能访问非静态的变量和非静态方法
- static代码块:仅在类加载时执行一次,可以将一些只进行一次的初始化操作放在static代码块执行
初始化顺序:父类静态变量和静态代码块 —— 子类静态变量和静态代码块 —— 父类实例变量和普通代码块 —— 父类构造函数 —— 子类实例变量和普通代码块 —— 子类构造函数
-
final关键字
- 修饰类:不可继承
- 修饰方法:不可被重写
- 修饰变量:值不可变
-
this关键字
- 调用当前类的实例变量,this.aid = aid
- 调用当前类的方法,this.mehtodName()
- 调用当前类的构造函数,this()
-
super关键字
- 引用父类的实例变量,super.aid = aid
- 引用父类的方法,super.methodName()
- 引用父类的构造函数,super()
-
面向对象的三大特性:封装、继承、多态
- 封装:隐藏了对象的属性和实现细节,仅对外公开接口,通过使用访问权限修饰符来定义访问级别。比如类就是将类的变量和方法实现隐藏在类中,通过类才能访问类的变量和方法
- 继承:子类可以继承父类的特征和行为,也可以拓展自己的特征和行为。单继承,一个子类只能有一个父类。
- 多态:实现多态的三要素:继承、重写、父类引用指向子类对象和接口引用指向实现类对象
- 静态多态性:通过方法的重载实现,即相同的方法可以有不同的参数列表,实现根据参数不同做出不同的逻辑处理。在代码编译时就可以根据参数列表不同区分
- 动态多态性:子类重写父类方法或者接口实现类重写接口方法,代码运行期间通过引用对象的不同调用不同的方法。比如子类cat,父类animal,都有eat()方法。Aniaml cat = new Cat(),cat.eat()调用的是子类cat的方法
-
抽象类与接口:
- 修饰:abstract class 和 interface
- 实现:extends 和 implements。子类和实现类都必须提供方法实现
- 继承:一个类只能继承一个抽象类,但可以实现多个接口
- 访问修饰符:抽象方法有public、protected、default。接口方法默认是public
- 构造函数:抽象类可以有构造函数,接口没有构造函数
-
变量
- 类变量:方法外,static修饰
- 实例变量:方法外,没有static修饰
- 局部变量:方法中
内部类:成员内部类、局部内部类、匿名内部类、静态内部类
-
重写和重载
- 重载:在同一个类中,方法名相同但参数列表不同
- 重写:@Override在父子类之间,方法名、参数列表相同,子类重写的方法的访问修饰符小于等于父类
- 构造函数可以被重载不能被重写
-
hashcode和equals
- 如果两个对象调用equals比较返回true,那么他们的hashcode一定相同
- 如果两个对象的hashcode相同,调用equals比较不一定为true
- 重写equals要重写hashcode,是为了保证equals返回true的情况下,hashcode也相同。如果重写了equals没有重写hashcode,就会出现两个对象相等但是hashcode不同的情况,这时当其中一个对象作为key保存到hashMap中,再通过另一个对象从hashMap中获取时,会拿不到
-
反射:在运行状态中,对于任何一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用它的任意一个方法和属性。(例子:spring的ioc、项目执行groovy脚本)
- clazz.getMethod()
- clazz.getDeclaredMethod()
- clazz.getDeclaredFields()
- clazz.getFields()
- 设置访问权限:field.setAccessible(true)、method.setAccessible(true);
- 反射为什么慢:https://juejin.cn/post/7330115846140051496
-
当编译一个类之后,会产生一个.class文件,该文件内存放着class对象。类加载相当于class对象的加载,而反射可以在运行时通过.class字节码问题件生成类并实现对象的增强
- Field:变量级别,get和set
- Method:方法级别,invoke()调用
- Constructor:构造函数,new instance()
2. 集合
- List:有序可重复
-
ArrayList:数组,初始容量10,线程不安全
- 扩容:默认情况下,扩容为原来的1.5倍,然后将原数组通过Arrays.copyOf()复制到新数组中
- 如果在遍历arrayList的过程中移除一个元素
使用for循环或者forEach会报错,可以使用迭代器Iterator.remove()
-
ArrayList和LinkedList
- 都是线程不安全
- ArrayList是数组,LinkedList是双向链表
- ArrayList扩容为原来的1.5倍,LinkedList是指针后移
- 访问、插入和删除元素:
- ArrayList增删时间复杂度为On(找到对应的位置n),查询时间复杂度O1(通过索引定位)。适合多查询少增删
- LinkedList增删时间复杂度为O1(指针指向),如果是指定index的插入,时间复杂度On,查询时间复杂度On(要遍历链表)。适合多增删少查询
- 绝大部分情况下直接使用arraylist,linkedlist作者自己也调侃从来没使用过linkedlist
-
ArrayList和Vector
- 底层都是数组
- Vector线程安全,使用synchronized修饰,性能不如ArrayList
- ArrayList扩容1.5倍,Vector扩容2倍
-
-
Set:无序不可重复
-
HashSet:底层是HashMap,初始容量16,负载因子为0.75的hashmap。hashset的值放在hashmap的key上,value为PRESENT。
- hashSet如何实现数据唯一
先计算add对象的hashcode来确定对象存储的位置,如果该位置没有其他对象,则将add对象存放到该位置。如果有其他对象,则通过equals判断两个对象是否相同,相同则存储失败,不同则重新计算存储位置
- hashSet如何实现数据唯一
TreeSet:红黑树
-
-
Map:kv键值对,通过key获取value,key唯一,value不唯一
-
HashMap:初始容量16,负载因子0.75,阈值为负载因子*容量
- JDK1.8之前:数组+链表
- JDK1.8之后:数组+链表+红黑树,链表长度大于8时,将链表转换为红黑树,当红黑树节点小于6时转换为链表
-
HashMap扩容:阈值为负载因子*容量,当元素数量大于阈值时,会扩容2倍的数组代替原数组
- 1.8之前:头插法,链表元素顺序发生改变,多线程情况下会出现循环链表问题
- 1.8之后:尾插法,链表元素顺序没有变化
-
HashMap的put:JDK1.8之后:数组+链表+红黑树
- 先判断数组是否为空,如果为空,则新建数组,长度16
- 使用hash算法计算key的索引,如果索引处没有元素,则将kv存入table[idx]处,如果table[idx]处已经有元素,如果table[idx]是树节点,则按照红黑树结构存入kv键值对,如果是链表节点,则插入链表尾端
- 如果插入后链表长度大于8,则转换为红黑树
- 插入成功后检测是否需要扩容
-
哈希冲突:在计算key的hash值后,会对数组长度取余,然后得到idx位置,所以不同的key计算得到的idx是可能相同的,这就是hash冲突
- 拉链法:用链表的形式,将Node节点(kv键值对)挂在table[idx]
- 红黑树:如果数据多,hash冲突严重,导致链表长度过长,当链表长度大于8时,转换为红黑树,提高查询效率
HashMap长度为什么是2的幂次方:因为hashmap存放kv时会先对key进行hash值计算,然后对得到的hash值进行数组长度取余,最终得到key存放的位置idx。而数组下标计算的方法是(length-1)&hash,长度为2的幂次方的话,等同于hash%length,提高了计算效率
-
为什么线程不安全
- JDK1.8之前,扩容后转移链表元素,采用头插法,多线程情况下可能出现循环链表问题
- JDK1.8之后,多线程情况下put元素,出现数据覆盖
-
HashMap和HashTable的区别
- HashMap初始容量16,扩容2倍。HashTable初始容量11,扩容2倍+1
- HashMap线程不安全,HashTable线程安全(get和put使用synchronized)
- HashMap可以接受key和value为null,当key为null时,存放在table[0]的链表,HashTable的kv不能为null
-
TreeMap:是有序的kv集合,对传入的key进行大小排序
- floorEntry:返回小于等于
- ceilingEntry:返回大于等于
LinkedHashMap:每次put会将entry插入到链表的尾部,实现有序的存储,遍历时能根据元素存入的顺序遍历
-
Queue:队列,后进前出(用作聊天记录,设置记录条数上限,add最新的一条会把最旧的一条顶掉)
-
ConcurrentHashMap:线程安全的hashMap
-
put
- 1.8之前,采用分段锁+Entry节点实现,每个分段锁维护一段数组,多个线程可以同时访问不同的分段锁,并发提高
- 1.8之后,Cas + synchronized + Node节点实现,锁住Node节点,锁粒度小。如果table[idx]位置为空,则通过CAS方式存入元素。如果不为空,则取出node节点,synchronized加锁,如果是链表节点则插入链表尾部,如果是红黑树节点则存入红黑树
扩容:在transfer转移方法中设置一个长度,表示一个线程处理的数组长度,最小值是16,在长度范围内只会有一个线程对其进行复制转移操作
-
跟HashTable的区别
- hashtable通过synchronized修饰,会锁住整个数组,而concurrenthashmap通过锁node节点,降低了锁的粒度,提高了性能
- hashtable初始容量11,扩容2倍+1。concurrenthashmap初始容量16,扩容2倍
-
-
CopyOnWriteArrayList:线程安全的List
- add方法加锁处理(reentrantlock),保证了多线程写入是不会出现线程问题
- 使用Arrays.copyOf()将数据复制到新数组
- 内存占用,写时复制机制,在写入数据时,内存中存在两个对象的内存
- 不能保证数据一致性,可能会读到旧数据
3. 多线程
1.线程池
2. 线程
进程是程序的一次执行过程,是系统运行的基本单位。
线程是比进程更小的执行单位,一个进程可以包含多个线程
并行:单位时间多个处理器同时处理任务
并发:单个处理器处理多个任务,按照时间片轮流处理
即使是单核的处理器也支持多线程,处理器会给每个线程分配CPU时间片,线程根据拿到的时间片去执行任务,因此多线程会经常进行线程切换,当切换到下一个线程时,当前线程会保存当前任务的执行状态,等待再次拿到时间片再继续执行任务
-
多线程的优缺点
- 优点:当一个线程进入阻塞或者等待状态时,CPU可以先去执行其他线程,提高了CPU的效率
- 缺点:
- 频繁进行线程切换,上下文切换,影响执行速度
- 死锁问题
-
死锁如何产生,如何避免
- 产生:两个或两个以上的线程互相竞争对方的资源,而且同时不释放自己持有的资源时,发生死锁,导致所有线程同时阻塞
- 条件:
- 互斥条件:一个资源在同一时刻只能由一个线程持有
- 请求与保持状态:一个线程在请求获取资源时发生阻塞,同时它持有的资源不释放
- 循环等待条件:发生死锁时,所有线程一致阻塞
- 不剥夺条件:线程已获得的资源在未使用完时不能被其他线程剥夺,自能由自己使用完后释放
- 避免:破环死锁产生的条件
- 互斥条件无法被破坏,因为锁的作用就是使线程互斥
- 破坏请求与保持条件:一次性请求所有的资源
- 破坏循环等待条件:按顺序来申请资源
- 破坏不剥夺条件:在申请不到资源时,释放自己持有的资源
-
线程的生命周期
- 初始:线程被创建,还没有调用start()
- 运行
- 阻塞:一般是被动的,在竞争资源时得不到资源,被动挂起在内存,等待资源被释放将其唤醒。
- 等待:进入该状态的线程需要等待其他线程的动作(通知或中断)
- 超时等待
- 终止:执行完毕
-
线程中断:线程在运行过程中被其他线程打断
- interrupt():给目标线程发送中断信号
- interrupted():判断目标线程是否被中断
-
创建线程的方式
- 继承Thread类,重写run()方法,调用start()方法
- 实现Runnable接口,重写run()方法,创建实例对象,将实例对象作为参数创建Thread对象,调用start()方法
- 使用Callable和Future
- 创建Callable接口的实现类,重写call()方法
- 使用实现类的实例化对象创建FutureTask对象
- 使用FutureTask对象作为参数创建Thread对象
- 调用start()方法
- 线程池
-
Runnable接口和Callable接口
- 相同点:都是接口,都需要调用Thread.start()启动线程
- 不同点:Callable是call()方法,有返回值。Runnable是run()方法,没有返回值
-
start()方法和run()方法
- run()方法定义执行的逻辑,start()方法启动线程
- run()方法可以多次被执行,start()方法只能调用一次
-
线程的其他方法:wait()、sleep()、notify()、notifyAll()、join()、yield()
-
sleep()方法和yield()方法
- sleep()方法会使当前线程暂停指定的时间,没有消耗cpu时间片
- sleep()方法使线程进入阻塞状态,yield()对cpu进行提示,使得上下文进行切换,线程进入就绪状态
- sleep()一定会完成指定的休眠时间,yield()不一定
- sleep()需要抛出InterruptException
sleep()方法和wait()方法
-
线程通信方式
- 锁和同步
- wait()、notify()、notifyAll()
- 信号量
- 管道
为什么wait()、notify()、notifyAll()是在Object类中