java
基础
基础
特性
简单,面向对象,可移植,解释执行(先编译为class文件,然后jvm解释执行)
修饰符
作用域
private(同类)、protected(同包和子类)、default(同包)、public(所有)
static
static修饰的变量内存只有一份,static修饰的变量和方法可以直接通过类名访问。static修饰的代码块只会执行一次
final
final修饰的变量成为常量,final修饰的方法不能被覆盖,final修饰的类不能被继承
super
可以调用父类的方法和属性
数据类型
基本数据类型
整数型(byte,short,int,long)
浮点数型(float,double)单双精度
字符型(char 2)
布尔型(boolean 1)
引用数据类型
类和接口、数组
变量类型
常量(final 修饰,无默认值,存在常量池)
类变量/静态变量(static修饰,有默认值,存在方法区)
成员变量/实例变量(方法外,有默认值,存在堆中)
局部变量(方法内,无默认值,基本数据类型存在栈中,引用数据类型的变量在栈,但指向的对象在堆中)
面向对象
定义
面向过程注重事情的每个步骤及顺序,面向对象注重参与事情的对象有哪些,以及他们各自需要做什么
三大特性
封装
将类的某些信息隐藏在内部(private),对外通过方法调用,不用关心具体实现。减少耦合
继承
从已有的类中派生出新的类,能继承并扩充已有类的属性和方法(父类private不能继承)
多态
父类或接口定义的引用变量可以指向不同子类的实例对象,实现对引用变量的同一个方法调用,执行不同的逻辑。让代码更好的扩展和维护。
特殊类
抽象类(abstract)
“是不是”的概念,1.不能创建对象,只能被继承;2.可以定义抽象方法。
接口(interface)
“有不有”的概念,1. 方法都是抽象的,变量都是final修饰的常量。2. 不能创建对象,只能被类实现或者被接口继承。3. 1.8后新增default方法,不是抽象方法,不用实现。
内部类
作用
1. 内部类可以更方便访问外部类成员(否则只能通过外部类对象访问),2. 每个内部类都能独立的继承一个接口的实现,使多继承变得更加完善.
注意
1. 内部类和外部类可相互访问private属性。2. 非静态的内部类不能定义静态方法和变量。
调用内部类变量或方法
1. 间接调用(通过外部类方法调用)。2. 直接调用(定义内部类对象调用)Outer.Inter inter=new Outer.new Inter();
匿名内部类
new对象时,直接接创建类new X{public void f(){…}}
高阶
泛型
含义
类定义时不设置属性或方法具体类型,实例化的时候再指定具体类型。优点:代码重用、保护类型的安全以及提高性能。
泛型通配符
class A{}:可以定义任意类型
class A{}:只能定义B或B的子类
class A{}:只能定义B或B的父类
反射
具体:反射就是先得到Class类对象,再通过该对象获得它的成员变量/构造方法/成员方法,最后分别通过成员变量/构造方法/成员方法的对象调用对应的方法
作用:可以动态获取类的信息, 提高代码灵活度,框架中比较常见
具体流程:
准备阶段:编译期将每个类的元信息保存在Class类对象中
获取Class类对象:三种方法x.class/x.getClass()/Class.forName()
实际反射操作:根据获取的Class对象,来获取属性,方法,构造方法(Filed/Method/Constructor)
动态代理
作用:类似Spring的aop,可以动态的,不修改源代码的情况下为某个类增加功能,如在一个方法的前后添加一下功能。
异常
Throwable
Error(错误):程序无法处理
栈/内存溢出
虚拟机运行错误
Exception(异常):程序可以处理
RuntimeException及其子类:如下标越界
非RuntimeException异常:可查异常,需要try catch 或throws。如文件上传
IO
字节流:以字节为单位,可处理任意类型数据
字符流:以字符为单位,一次性可读多个字节,处理字符类型数据。
集合
collection(单列集合)
list(可重复,有序:元素存取顺序一样)
查快:ArrayList
底层数组,查快,增删慢,线程不安全,效率高
ArrayList的扩容:使用无参构造方法时,初始大小是0,当有数据插入时,扩展到10。每当容量到达最大量就会自动扩容原来容量的1.5倍(会把老数组元素重新拷贝一份到新数组,代价较高,所以知道初始容量,可以初始化时指定一个初始容量)。
线程安全
Vector:线程安全,结构跟ArrayList类似。内部实现直接使用synchronized 关键字对 每个方法加锁。性能很慢。现在淘汰(synchronizedList跟Vector类似,区别是使用synchronized 关键字对 每个方法内部加锁)
CopyOnWriteArrayList:线程安全,在写的时候加锁(ReentrantLock锁),读的时候不加锁,写操作是在副本进行,所以写入也不会阻塞读取操作,大大提升了读的速度。缺点:写操作是在副本上进行的,数组的复制比较耗时,且有一定的延迟。
删快:LinkedList
底层双向链表(所以可以作为栈和队列),查慢,增删快,线程不安全,效率高
set(唯一,无序:元素存取顺序不同)
未排序:HashSet
底层hashMap(对应value为object常量对象),先判断对象hashcode是否重复,如果不重复那肯定就没添加。如果重复再通过equals比较。
有一个子类LinkedHashSet,底层为链表和哈希表,依赖链表保证有序
排好序:TreeSet
底层红黑树(Compareable保证唯一)
map(双列集合,无序)
未排序:HashMap
线程不安全,底层数组+链表+红黑树(1.8之前为数组+链表)。(扩展:1.8后链表插入从头插法改为了尾插法,因为头插法在多线程中可能导致死循环:在扩容时,头插法打乱了链表的顺序,第一个线程扩容后,顺序相反。此时第二个线程再进行扩容时,就会出现死循环,本来a节点的next时b,单第一个线程扩容后b的next节点是a)
hashMap添加和扩容
put添加过程
1. 根据key获得哈希码,计算数组下标位置。
key通过hashcode获得hash码
位置计算:hash码&(length-1),比 hash值%数组长度 更快
2. 如果对应位置没有元素 就直接把封装好的对象放到该位置。如果对应位置是红黑树节点,就把新节点放到红黑树节点上(期间会判断是否存在key,存在就更新)。如果是链表节点,就通过尾插法插入链表节点,然后判断节点数大于等于8就转为红黑树。
3. 最后判断是可否需要扩容
扩容机制(默认大小16)
先生成新数组(默认原两倍),然后遍历老数组每个元素,计算对应新数组位置。如过某个位置元素大于等于8就转红黑树。
加载因子
默认0.75(hashmap默认大小16,乘以加载因子为12,所以达到12就进行扩容。)加载因子越大,空间利用率越高,但冲突机会增加。
线程安全的
线程安全的HashTable,底层数组+链表,通过Synchronized对整张表加锁保证安全,现已淘汰
线程安全的concurrentHashMap
1.7是segment数组+hashEntry实现。一个segment中包含一个hashEntry数组,hashEntr是链表结构。线程安全是通过ReentrantLock对Segment数组加锁实现,
1.8后是数组+链表+红黑树实现。线程安全是通过synchronized对头节点加锁保证线程安全,相对1.7减少了加锁的粒度。(扩展:1. 读不需要加锁因为使用volatile对元素进行修饰,其他线程修改时是可见。2. 初始化和添加元素通过cas和volatile保证线程安全)
根据key排好序:TreeMap
底层红黑树,各操作O(logn)
线程
状态
新建状态,new一个线程后
就绪状态,调用start后
运行状态,执行run方法
阻塞状态,如调用sleep,wait等方法后
终止状态,如线程正常结束,或者调用stop等
开启线程三种方式
继承Thread类,重写run方法。(代码:Thread t=new MyThread()😉
实现Runnable接口,在类中实现run()方法(代码:Thread t=new Thread(new MyThreadt())😉
实现Callable接口,实现call方法,再结合FutureTask 创建线程(可获取线程结束后的返回值)(代码:Thread t = new Thread(new FutureTask<>(new MyCallable));)
比较:接口实现优势:1.避免java中的单继承的限制,2. 使用接口的方式能放入线程池。(Callable较Runnable而言,线程可以有返回值)
线程相关方法
Thread.sleep(long millis) :线程睡眠,线程转到阻塞状态(不释放锁)
Obj.wait() :线程等待,线程转到阻塞状态(释放对象锁,需要notify唤醒)
Obj.notify(): 线程唤醒,唤醒等待中的线程(注:它不是立即唤醒,notify()是在synchronized语句块内,在synchronized(){}语句块执行结束后当前线程才会释放对象锁,唤醒其他线程)
Thread.yield() :线程让步,让相同优先级的线程之间能适当的轮转执行
join(): 线程加入,在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
interrupt():线程中断,向其他线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常。
注:线程优先级范围1-10,默认为5。main()本身也是一个线程,当main结束后,子线程仍会继续执行到结束。每个程序至少有两个线程,main和垃圾回收线程(每个程序启动都会启动一个jvm)
线程池
定义
可以容纳多个线程的容器,其中线程可以反复使用,避免反复创建线程的开销(同时新来任务也不用去创建线程比较快)。
创建线程池步骤
创建线程池:ExecutorService service = newFixedThreadPool(5);//(读音 e g z ki ter) 另一种创建方式: ExecutorService service = Executors.newFixedThreadPool(5);//
加入线程:service.submit(new MyCallable());//或者service.submit(new MyRunnable ());加入后会自动调用run方法
关闭线程池:service.shutdown();
线程池的七大参数
常驻核心线程数(corePoolSize):线程中长驻的核心线程
最大线程数(maximumPoolSize):大于等于1
多余的空闲线程存活时间(keepAliveTime):线程数超过常驻核心线程数时,多余线程的空闲时间达到keepAliveTime时就会销毁
unit:keepAliveTime的单位
任务队列(workQueue):被提交但是尚未被执行的任务。当任务达到核心线程数过后,就会放到任务队列中,任务调度时候再取出
线程工厂(threadFactory):用于创建线程
拒绝策略(handler):当工作线程大于等于最大线程数且任务队列也满了时,就会触发拒绝策略(4种:1. 异常策略(AbortPolicy):直接抛出异常。2. 丢弃策略DiscardPolicy:新来任务被提交后直接丢弃。3. 淘汰最久任务策略DiscardOldestPolicy:丢弃存活时间最长的任务。4. 执行策略(CallerRunsPolicy):让提交任务的线程自身去执行该任务)。
线程池执行流程
提交任务
判断核心线程是否满,没满创建线程执行提交任务(就算有空闲线程也会创建,保证创建满核心线程数)
核心线程满判断任务队列是否满,没满放任务队列,等核心线程空闲再去执行。
任务队列也满,判断最大线程数是否满,未满创建线程,满就触发拒绝策略
ThreadLocal
介绍:线程本地存储机制,它可以将数据缓存到某个线程内部,因为共享区的数据为了安全会使用加锁等机制,从而影响效率。
底层:每个线程都有一个ThreadLoacalMap:数据是以key为ThreadLocal对像,值为数据缓存到ThreadLocalMap中的,
场景:比如把用户信息存入Token中,当用户调用接口时,拦截器中解析Token(header中携带 Token),把用户信息存在ThreadLocal中,然后就可以在controller,service等多层方便的共享用户数据,并且能保证对每个请求,都只能访问当前请求的用户信息
注意:因为线程池中的线程不会销毁,所以ThreadLocal对应的值也不会被回收会导致内存泄漏,所以建议在使用完后remove掉。
和加锁对比:加锁用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
锁(保证线程安全)
Synchronized和ReentranLock
不同点
Synchronized是一个关键词(jvm层面的锁),ReentranLock是一个类(API层面的锁)
Synchronized可以修饰方法和代码块(在方法前加static 可锁住类的所有对象),Jvm自动释放锁。ReentranLock只能修饰代码块。需要手动释放锁
Synchronized锁的是对象,锁信息保存在对象头中的Markword中(记录四个锁状态),ReentrantLock锁的是线程,锁的信息保存在AQS 中
相同点:Synchronized和ReentranLock都是可重入锁
就是在一个线程中允许你反复获得同一把锁,若多次加锁记需要多次解锁才能释放(实现:每个锁关联一个请求计数器和一个获得该锁的线程,加一次锁计数器加1)
为什么需要可重入锁:最大程度避免死锁(对象中加锁的A方法调用加锁的B方法,就会导致死锁,可重入锁允许一个线程反复或的一把锁,就不会导致死锁)
ReentrantLock 相对 synchronized 多了三种功能
1.等待可中断(正在等待的线程可以选择放弃等待,改为处理其他事情。)
2.可实现公平锁(默认非公平)
公平锁:在获取锁时,会先检查AQS同步队列是否有线程在排队,有就进行排队(AQS先进先出的双向队列)
非公平锁:获取锁时不会检查是否有线程在排队,而是直接竞争锁(如果没竞争到锁,后面就跟公平锁一样也会去排队,当锁释放只会唤醒AQS队列的首个线程,非公平只体现在加锁阶段,不体现在唤醒阶段)
3.可绑定多个条件,实现有选择的唤醒等待(synchronized结合notifyAll()会唤醒所有等待的线程。Reentrantlock中一个锁对象可以创建多个对象监视器,线程对象可以注册在指定的对象监视器中,从而可以有选择性的进行线程通知)
Synchronized相关
为什么说Synchronized是一个重量级锁
Synchronized 底层实现依赖于操作系统的互斥锁。而操作系统实现线程之间的切换需要从用户态转换到核心态,转换耗时,成本非常高。这种依赖于操作系统互斥锁的称为重量级锁。
锁机制如何升级
1.6之前就直接是重量级锁,但效率比较低,所以后面引入锁升级机制,用于平衡安全和效率的问题 。
偏向锁:在对象头中记录当前获得该锁的线程id,下次该线程就可以通过偏向锁直接获取到资源。(因为大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁)
轻量级锁:每个竞争线程通过自旋不停看被锁住资源是否释放。避免用户态到内核态的切换。
重量级锁:由操作系统来判断锁住资源是否释放,释放再通知其他线程。
过程
先尝试用偏向锁的方式去竞争资源。
如果失败就表示其他线程已经占用了偏向锁,此时升级成轻量级锁,通过自旋的方式尝试去加锁。
如过多次竞争失败就会升级成重量级锁(因为太多线程不断自旋竞争对效率影响也比较大),此时没竞争到锁的线程会被阻塞,
双重检查锁DCL(Double Check Lock)
作用:尽可能减少加锁的范围(锁如果加在第一重,初始化除了创建对象可能还涉及其他赋值操作,所以加锁范围太大。在初始化中创建 对象为什么还要二重检查?因为第一重检查未加锁,所以可能导致多个线程都判断对象为空,从而进入初始化方法,依次创建多个对象)
volatile
作用:保证可见性(强制将修改的值立即写入主存)、有序性(禁止进行指令重排序)。
扩展:普通共享变量修改后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
指令重排:指令重排序是编译器处于性能考虑,源码顺序和程序顺序可能不一样。例子:比如new一个对象,底层是三步(1.分配内存空间。2. 执行构造方法,初始化对象。3. 把这个对象指向空间。)所以可能导致A线程执行了1,3。b线程判断此时对象为空,然后又执行初始化操作。
voalate和synchronized区别:
并发编程三个重要的特性是原子性、有序性、可见性。
原子性:synchronized通过互斥锁。voalate不保证原子性(可以和cas操作搭配保证原子性)
有序性:synchronized通过程序串行化执行,volatile通过禁止指令重排。
可见性:volatile通过强制将修改的值立即写入主存,synchronized通过jvm指令monitorexit把共享资源都刷新到内存保证
CAS操作:非阻塞原子性操作(通过硬件保证原子性),同一时刻只有一个线程可以修改,其他线程并不会阻塞而是重新尝试
ReentrantLock相关
使用:先创建一个ReentranLock对象,然后通过lock()和unLock()方法加锁和释放锁。还有tryLock()尝试加锁,可以用于自旋。
加锁过程
ReentrantLock中包含一个AQS对象,这个对象中又三个核心变量:1. 加锁状态state(0表示未加锁)2. 加锁线程。3. 等待队列(Node双向链表)
加锁过程:线程尝试通过CAS操作将state值从0变为1,如果修改成功就把加锁线程设置为自己。修改失败就看看加锁线程是否为自己,不是就进入等待队列。当持有锁的线程释放后会唤醒队列首个线程
AQS
定义:AQS是java并发包的基础类,java并发包下很多API都是基于AQS来实现的加锁和释放锁等功能的,如ReentrantLock。
JUC线程并发库
并发容器类:ConcurrentHashMap(线程安全)
锁相关类:ReentrantLock
线程池相关的类:Callable,Executor(创建线程池的)
面试题
比较
深拷贝和浅拷贝
浅拷贝:只复制一个对象的引用
深拷贝:把对象完全复制一份
重写和重载
重写是子类重写覆盖父类的方法(注意:重写方法的 修饰符范围不能小于父类,且private方法不能重写)
重载是同一个类中方法名相同,参数不同
序列化和反序列化
序列化:把对象拆解成字节碎片(可用于对象的持久化)
java实现:先把需要序列化的类实现Serializable接口(标志作用),通过对象输出流ObjectOutputStream把对象转换成字节碎片,然后通过文件输出流FileOutputStream把字节碎片写入文件。(还可以指定一个序列化id号,防止进出版本不一致)
jdk、jre和jvm
jdk:它是java开发工具包。包含:java编译器,Java运行环境jre,java常用类库
jre:它是Java运行环境,包括jvm和jvm所需类库
jvm:java虚拟机,用于运行字节码文件(会将字节码文件解释为机器指令,不同操作系统机器指令不同,所以不同操作系统的jvm也不同,但都运行字节码文件)
、equals()和hashCode()
:如果是基本类型,则比较的是值,引用类型比较的是引用地址。
equals:具体比较什么是看类中equals方法重写逻辑(默认是Object中的equals跟是一样的)
hashcode:hashcode是获取一个对象的hash码,可以根据hash码找到对象在堆中的位置(堆中有一个hash表与之对应)。
hashcode扩展
两个对象相等,hashcode一定相等。hashcode相等,对象不一定相等。
为什么重写equals时也要重写hashcode方法:因为java中一些集合类比如hashset判断两个对象是否相等会先判断hashcode,所以如果不重写hashcode,就会导致该类与其他集合类一起工作时出问题。
hashcode意义:HashSet集合添加元素,需要判断是否添加重复元素。如果用equals一个个比较太慢了,所以可以先判断对象hashcode是否重复,如果不重复那肯定就没添加。如果重复再通过equals比较。
String、StringBuffer和StringBuilder区别
String:底层时final修饰的byte数组(所以不可被继承)。若频繁修改字符串时,会产生很多无用的中间对象,效率很低。
StringBuilder :底层也是一个byte数组,但不是常量,当容量不足时会进行扩容。线程不安全。
StringBuffer:在StringBuilder基础上考虑了线程安全。通过synchronized为每个方法加锁保证线程安全。效率较低
int和Integer
区别
Integer:Integer是int提供的封装类;Integer对象需要实例化,默认值为null。
int:int是基本数据类型,直接存储数值,默认值是0;
数值比较
Integer和int比较:Integer是int的封装类,int与Integer比较时,Integer会自动拆箱,无论怎么比,int与Integer都相等
Integer和Integer比较:通过equals比较,但对于数值在-128与127之间的Integer对象,会缓存在内存中。所以会直接从内存取,不会创建新的对象,所以也可以通过比较。
拆箱装箱原理:装箱是编译的时候自动调用vulueOf(),拆箱是调用intValue()方法。
run()和start()
run方法就是一个普通的方法,而start方法会创建一个新线程去执行run()的代码。
概念
new对象初始化顺序
父类静态代码块,子类静态代码块,父类构造方法,子类构造方法
构造方法有哪些特性
名字与类名相同,没有返回值,但不能用 void 声明构造函数,创建对象时自动执行
一致性hash算法
优化分布式环境下,主机增加或减少情况下hash映射的问题。普通hash会全部重新隐射。一致性hash通过hash环的数据结构来优化。把主机iphash后放到hash环上,然后对key hash后,放到顺时针方向离自己最近的主机上。所以主机增加或减少时只需少部分数据需要重新映射
具体:环范围0-2^(32-1),主机ip hash后放到环上,节点key hash后放到顺时针最近最近上。(数据倾斜可以通过虚拟节点解决——每个主机创建多个虚拟节点,放到环上)
java8新特性
lambda表达式
定义:语法更简单,本质就是创建一个接口实现类的具体对象。(对应接口只能有一个抽象方法,可以在接口加一个注解,限制其只能有一个抽象方法)
例子:X x = ()->System.out.print(“1”);//()对应的就为接口中无参方法,箭头后就为实现的内容。
作用:简化代码,适用于代码不复用的场景
函数式接口
可以在接口加一个注解,对应接口只能有一个抽象方法,与lambda连用
方法引用
lambda深层引用,如果在lambda表达式中,函数式接口的参数和返回值 和 方法体实现中的方法的的参数和返回值一样就可以使用
三种使用情况:
对象::实例方法名(非静态方法)例子:X x = y::方法名; x.方法名(参数)(y为方法体中的方法对应的对象)
类::静态方法名
类::实例方法名(还要满足条件:两个参数,第一个参数为方法调用者,第二个参数为方法的实际参数)
接口新增default方法
不是抽象方法,不用重写
修改一些底层实现
如hashmap,底层新增红黑树结构
新增一些api(日期相关)
为什么数组具有快速查找的能力
数组是连续的内存地址,且元素都是同一类型,占用空间大小一样,所以通过下标就可以计算出元素的位置。
浮点数
IEEE754标准
float占4字节,32位(1个符号位,8个指数位,23个尾数位)指数对应表示大小范围:8位指数大小范围是[-127,128]),所以表示数值范围是-2128到2128。尾数对应精度:23位尾数,能表示最大十进制为2的23次方为7位数,所以完整精确表示的为6位。(省略位为1,所以不算进去)
double占8字节,64位(1个符号位,11个指数位,52个尾数位)表示10进制精度位15位。表示数值范围为-21024到21024
十进制数值存储过程(以float为例)
先把10进制转换为2进制,然后规范化(科学计数法)。对于符号位:正数存0,负数存1。对于指数位:因为指数位8位范围是[-127,128],转二进制存计算机时,为了避免负数,加上一个固定偏移量127,然后转二进制存入8位指数位。对于尾数:由于规范化后首位都为1,所以省略首尾,将小数点后的位数放入尾数部分,不足补0。
例子:10.75转为二进制为1010.11,规范化为1.0101110^3。 正数符号位为0。尾数为省略首位后为01011。指数为3+127=130,换算为二进制为10000010。所以最终10.75存在计算机就为:0 10000010 01011000000000000000000
扩展:十进制转二进制
正数:除2取余,倒叙排列。如2:2/2得1取0;1/2得0余1取1。最后得10
小数:乘2取整,正序排列。如0.25:0.252=0.5取0,0.5*2=1.0取1。最后得到01
注意事项
1. 比较两个浮点不要用==,而是做差是否在一定范围(一般差小于10的-6次方)。2. 尽量使用double而不是float(因为float精度太低 )。3.金融场景一定要使用BigDecimal(无精度损失)
扩展
int占4字节,32位,除去符号位还剩31位。范围为[-231,231-1]。(因为31可以表示231个正负数。分别为[0,231-1]和[-231+1,-0]。因为只需要一个0,而且-0的原码加上符号位,恰好等于-0的补码,所以负数要多一个,最后范围为[-231,2^31-1])
正数:原码=反码=补码。负数:反码为原码符号位不变,其他位相反;补码为反码加1。(计算机都存的补码)