String、StringBuilder、StringBuffer
String不可变,StringBuilder和StringBuffer可变,只有StringBuffer是线程安全的。
String不可变的四个原因:
(1)类被final修饰不可被继承
(2)底层char[]数组被final修饰
(3)提供的方法返回值都是new一个新String对象
(4)字符串常量池可知,程序中字符串指向同一个对象,如果字符串可变,那么改变一个,另一个也会改变。
String的append方法与String不可变冲突吗:
不冲突!拼接字符串底层是用StringBuilder的append方法,而StringBuilder底层是调用AbstractStringBuilder方法的append方法,拼接的时候是复制char[]来实现append。最后返回是一个new String对象。
StringBuffer如何保证线程安全:
StringBuffer的方法用Synchronized声明了,是线程安全的。
"a"+"b":底层是new一个StringBuilder出来append字面量a和字面量b。
字符串常量什么时候被加载到字符串常量池中的:
对于这种字面量在编译的时候会加入class常量池中,然后运行期间首次被调用的时候才会在字符串常量池中创建一个String对象,也就是此时加入字符串常量池。
对于字符串常量池中字符串来源有两种,第一个是字面量在运行期间首次调用加入字符串常量池,第二个是运行期间将普通字符串对象通过intern方法加入。
String s = new String("a"); 请问创建了几个对象:
如果是第一次执行,则创建两个:创建一个字符串“a”放到字符串常量池中,然后在堆内存中创建一个对象,这个对象是字符串常量池中“a”的引用。
如果是第二次执行,则创建一个:创建一个对象指向字符串常量池中的对象。
String s = "ab"; String ss = "a"+"b"; sss结果是什么:
由于有字符串常量池的存在,s和ss都是字符串常量池中“ab”字面量的一个引用,而在java中比较的是地址,所以返回的是true。
String 有长度限制吗?
编译器与运行期不同。编译器表示CONSTANT_utf8_info表示字符串常量,限制2^16-1=65535.
运行期String的length属性是int类型,最大2^31-1.
java的各类常量池
a. Class常量池Constant Pool:
类编译成.class文件后会将字面量、符号引用放到class常量池中(编译期间),class常量池中的内容运行的时候会加载到常量池中(运行期间)。通过javap命令查看内容:javap -v xxx.class
b. 运行时常量池Runtime Constant Pool:
它是java虚拟机中的一个逻辑区域,或者说是一种规范,它指定运行时的常量、符号引用等数据应该放在运行时常量池,HotPot虚拟机就把它给实现了,放在了方法区。
c. 字符串常量池:
它是运行时常量池的一块逻辑子区域的一个具体实现,jdk1.7之后它位于堆内存。因为运行时常量池规定常量要可以做到复用,所以它存储的是字符串常量。
java有基本类型为什么还需要包装类
a. java面向对象,集合要求元素是Object对象
b. 包装类型相比基本类型有了更多的属性与方法,支持更多操作
c. RPC接口返回中使用的是包装类型,可以避免歧义
自动拆箱与装箱
基本数据类型与包装类型之间的转换。
例如:List
java中接口与抽象类的区别
a. 最主要的区别:接口用于制定规范,抽象类用于抽象出通用模板
b. 编码规范的区别:接口可以多实现,抽象类只能单继承;接口是对外公开的,修饰符都是public,抽象类修饰符没有限制;接口不能有私有方法与变量,抽象类可以有;接口的设计主要自顶而下,一般是先对下游提供接口文档,然后对接口做不同的实现。抽象类的设计主要自底而上,基于模板方法抽象出公共逻辑再扩充。
java的多态
a. 多态是一种运行期概念,同一种动作作用于不同对象会产生不同结果
b. 多态产生的条件:子类继承父类or实现接口、子类重写父类的方法、父类引用指向子类对象;
java的泛型
a. 定义类或者接口的时候使用类型作为参数
b. 作用:参数是类型参数,可以提高复用性;安全性,因为编译期间可以做类型检查,否则如果运行期间出现类型转换异常程序可能直接退出。
c. 类型擦除:编译之后的字节码文件中会将类型信息擦除。
ConcurrentHashMap
https://blog.csdn.net/Mind_programmonkey/article/details/111035733
HashMap不是线程安全的,HashTable是线程安全的,但是会使用Synchronized锁住整张表来保证强一致性,ConcurrentHashMap使用锁分段来降低锁粒度来提升并发性能,它在读的过程不加锁,写的过程加锁。
JDK 1.7:ConcurrentHashMap有两个内部类:HashEntry、Segment。它是一个Segment数组,而每个Segment元素又是一个HashEntry数组,HashEntry数组中每个元素的位置都是一个单向链表的头节点,由于HashEntry内部的next属性是final,所以插入元素使用头插法。
由于Segment继承了Reentrantlock,所以他本身就是一把锁,上锁的时候锁的是Segment。
JDK 1.8:取消了Segment,整体是HashEntry数组,锁粒度是单个HashEntry:当桶节点为空,则使用CAS+Synchronized的操作插入数据。如果桶节点不为空,则使用synchronized锁住桶节点,再插入数据
(1)为什么使用synchronized而不是reentrantlock?
答:
1、synchronized不需要手动加锁解锁
2、syn是JVM内置语义,编译器层面可以进行优化,例如锁粗化,锁消除
3、syn本身有一个锁升级的过程,加锁失败的线程不会挂起,在轻量级锁下而会自旋获取锁,减少线程唤醒的开销,只有重量级锁才会阻塞。
4、syn加锁的开销都没有reen那么大,只需要操作对象头或者监视器锁即可。
(2)ConcurrentHashMap的数据结构:
Map是由Segment数组+Entry数组+链表。一个Segment就是一张哈希表,Segment继承了ReenTrantlock,自己就是一个锁。
JDK 1.7中,ConcurrentHashMap查找元素需要两次定位,第一次定位到指定的Segment,第二次从Segment定位到元素所在链表的头部。这在提高并发性能的同时会导致Hash时间过长,并且最高同时支持Segment数量大小的写操作
JDK 1.8 (Java 8)中,进一步降低锁的粒度,对节点进行加synchronized锁,使用CAS算法
(3)ConcurrentHashMap中变量使用final和volatile修饰有什么用呢?
使用final:确保初始化的线程安全
使用volatile来保证某个变量内存的改变对其他线程的可见性,在配合CAS操作的原子性可以实现不加锁对并发操作的支持。
例如,get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的
(4)ConcurrentHashMap有什么缺陷吗?
同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。
(5)ConcurrentHashMap在JDK 7和8之间的区别
● JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
● JDK1.8版本的数据结构中没有Segment了,只有桶数组,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
● JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表(6和8就是阈值,不选7是防止频繁切换),这样形成一个最佳拍档
(6)HashMap如何实现线程安全问题
答:(1)使用Collections.synchronized(new HashMap<>()) 使其变成一个线程安全的Map。底层做的事情是用synchronized锁住整个Map
(2)降低锁粒度,封装一个HashMap,使用ReadWriteLock(互斥写锁、共享读锁)封分别封装put方法和get方法。
(3)ConcurrentHashMap
3、HashMap的认识
(1)HashMap的数据结构:
HashMap实际是一个桶数组,它底层使用拉链式散列算法解决哈希冲突,也就是用数组+链表/红黑树构成的,数组是主体,链表或者红黑树用来解决hash冲突,数组是Entry类型,每一个Entry包含key-value队。一个hash表就是一个数组,每个元素是哈希桶
(2)get方法:
- 首先,需要计算键的哈希值,并通过哈希值计算出在数组中的索引位置(桶的位置)。
- 如果该位置上的元素为空,说明没有找到对应的键值对,直接返回null。
- 如果该位置上的元素不为空,遍历该位置上的元素,如果找到了与当前键相等的键值对,那么返回该键值对的值,否则返回null。
(3)put方法: - 首先,put方法会计算键的哈希值(通过调用hash方法),并通过哈希值计算出在数组中的索引位置。
- 如果该位置上的元素为空,那么直接将键值对存储在该位置上。
- 如果该位置上的元素不为空,那么遍历该位置上的元素,如果找到了与当前键相等的键值对,那么将该键值对的值更新为当前值,并返回旧值。
- 如果该位置上的元素不为空,但没有与当前键相等的键值对,那么将键值对插入到链表或红黑树中(如果该位置上的元素数量超过了8,就会将链表转化为红黑树来提高效率)。
- 如果插入成功,返回被替换的值;如果插入失败,返回null。
- 插入成功后,如果需要扩容,那么就进行一次扩容操作。
(4)计算桶数组的位置公式:
hashmap定位数组索引哈希桶位置公式:(table.length - 1) & (key.hashCode ^ (key.hashCode >> 16))
X%table.length 等价于 X&(table.length-1),因为与运算效率高
X=(key.hashCode ^ (key.hashCode >> 16)),原因:
防止低位相同而高位不相同导致计算的hash值相同,例如容量是16,那么length-1=1111,它与运算的结果只取决于第四位,如果第四位相等则hash相等,这种碰撞概率很大。
因此我们让让hashcode的高十六位与第十六位异或,是一种哈希扰动,相当于把高16位信息保存下来了,可以增加随机性,减少哈希碰撞。
(5)为什么容量必须是2的n次方:
第一,转化为与运算效率最高。第二,保证均匀性,减少hash碰撞,例如容量是17,X&(17-1),最后的结果只有16和0,这样大大增加哈希碰撞概率。
(6)链表与红黑树转化:
哈希桶元素个数大于8转红黑树,8的原因是官方假设hash函数服从泊松分布,元素个数为8的概率非常非常小,而红黑树虽然在元素多的情况下能够提升查询的效率,但是它的存储容量也是链表的两倍,因此只有在小概率事件上才会转红黑树。
哈希桶元素个数小于6转链表,6的原因是防止链表与红黑树的频繁转换,这样耗费性能。
(7)redis与hash的关联:一个redis就是一张全局hash表
redis就是一个dictEntry数组,dictEntry内容如图:key是SDS、value是redisObject
结构一共定义了 4 个元数据和一个指针:
type:redisObject 的数据类型,面向用户的数据类型(String/List/Hash/Set/ZSet等)。占用 4 bit
encoding:redisObject 的编码类型,是 Redis 内部实现各种数据类型所用的数据结构,每一种数据类型,可以对应不同的底层数据结构来实现(SDS/ziplist/intset/hashtable/skiplist等)。占用4bit
lru:redisObject 的 LRU 时间。占用24bit
refcount:redisObject 的引用计数。占用4个字节
ptr:指向值的指针。占用8个字节。
redis的rehash:
redis有两张表,用户rehash的时候交替保存数据。一张表用于接收服务请求,另一张表用于rehash迁移表。
触发rehash条件:
负载因子:hash承载元素/当前设定的大小 >1 的时候会rehash;但是在持久化时不rehash
rehash之后扩容多大:和hashmap一样,扩大两倍。
渐进rehash:把很大的数据迁移工作,分摊到多次小的操作中。
● 增删改查哈希表时:每次迁移 1 个哈希桶
● 定时 rehash:如果 dict 一直没有操作,无法渐进式迁移数据,那主线程会默认每间隔 100ms 执行一次迁移操作。这里一次会以 100 个桶为基本单位迁移数据,并限制如果一次操作耗时超时 1ms 就结束本次任务,待下次再次触发迁移
(8)hashmap的扩容
扩容条件:
元素的数量达到总数*负载因子的时候会扩容
某个桶里链表长度大于8且总元素数小于64。或者说 shu
new一个hashmap第一次往里面加元素的时候(默认扩到16)。
为了保证容量是2的幂次,每次会扩大到原来的两倍。
扩容的时候会做三件事情:
JDK1.7 的时候会重新计算hash,用头插法插入新链表,链表顺序会倒置。
JDK1.8 之后:
第一、如果桶里只有一个元素,直接rehash。
第二、如果桶子里是链表或者红黑树,则需要划分成low(低位)和high(高位)两部分,划分的依据就是hashcode & oldcap(旧容量)0,如果等于0,则为low部分,位置不动,如果不等于0,则新的索引位置就是旧索引+旧数组长度。
第三,如果红黑树被划分之后,元素个数小于6个,会退化为链表(红黑树TreeNode里面继承了Node,有pre属性可以退化为链表)。
(9)hashmap和hashtable的key可以为null值吗?
答:
hashmap的key可以为null值。原因是底层计算hashcode的时候会特殊处理,null的hash值就是0,但是只能有一个,因为俩null值没法区分。
hashtable的key不能为null值,原因是底层没用hashcode,不能处理null,更重要的是它是线程安全的,如果key是null就不知道是存在null?还是这个key本来就是null。
(10)为什么负载因子是0.75?
1、如果设置成1,那么只有满了的情况下才会扩容,最好的情况就是16个元素分别不同的桶位置,否则hash碰撞概率会大大增加,查找速率会变慢。
2、负载因子太大会hash冲突,太小又会造成资源浪费,官方权衡之下进行了数学推理,发现0.7左右非常合适,但是容量是2的n次幂,那么取0.75正好可以把扩容阈值设为整数。
(11)初始容量的选择
期望插入的数据量n,计算n/0.75 +1,然后向上找到大于它最小的2次幂即可。
(12)JDK1.8 hashmap发生了什么变化?
1、解决hash冲突,链表变成红黑树
2、Entry变成Node,且Node节点hash类型是final
3、hash表计算索引的方法由异或运算变成与运算,并且加入了hash扰动
4、扩容算法发生变化,transfer移动到了resize里面
5、扩容算法由头插法变成尾插法。
(13)如何解决hash冲突
链地址法:冲突的元素存在链表里
开放地址法:线性探测,例如ThreadLocalMap(一直往后找空位)、二次探测再散列法(i±1、i±3的找),双重哈希(先算一个hash值p1,如果冲突了就用p1再哈希得到p2...,直到不冲突)
再hash法:用多个不同的哈希函数,第一个发生冲突就用第二个...直到无冲突
建立公共溢出区:把哈希表分为基本表和溢出表,发生冲突的放到溢出表。
(14)如何理解多态?
答:同一个操作作用于不同的对象可以产生不同的结果。(例如:调用run方法(同一个操作),A.run(),B.run()执行完的结果不同)
多态实现有三个条件:
1、实现接口或者继承父类
2、重写方法、
3、父类的引用指向子类对象。
多态是一种运行期的概念,它是根据代码运行实时决策出来的,让代码更灵活。维基百科上说多态分为静态多态和动态多态,有人认为函数的重载就是一种静态多态,但我不这么认为。
重载是同名不同参的方法,编译期间会根据参数类型来决定调用哪个方法。
重写是子类重写父类的方法。
重载是编译期绑定,写代码的就能根据具体参数决定调用。重写是运行期绑定,会根据父类引用具体指向的子类对象来决定调用哪个方法。
(15)final、finally、finalize区别
答:只是长得像,实际没什么关系
final:修饰变量,则不可变,修饰方法,则不可重写,修饰类,则不可被继承
finally:用于异常处理,程序抛异常后finally代码块的内容一定会执行,用于释放资源,比如连接对象,锁,threadLocal。除非JVM进程退出、线程执行过程中抛了Error异常。
finalize:用于在GC之前清理资源
(16)和equals的区别
:对于基本数据类型,比较的是值相等。对于引用数据类型,比较的是内存地址是否相等。
equals:对于原始的Object类中的equals方法,底层也是调用,但是后续的类重写了equals方法,例如String类的equals方法,首先比较两个变量指向的内存地址是否相等,也就是调用==,其次会判断二者的类型是否相等,最后再判断内容是否相等。
(17)equals与hashcode:
hashcode是Object中的方法,所以所有类都有hashcode方法。对于set集合而言,它不可重复,那么我们在插入元素的时候就需要判断元素是否已经存在,但是如果元素过多的话,一直调用equals判断会非常麻烦,所以集合就会重写hashcode方法来通过hashcode方法进行一系列计算拿到元素在集合中的索引位置,如果这个位置有元素,则需要使用equals方法来比较二者是否相等,这样就省去很多调用equals方法。
(18)为什么要重写equals方法和hashcode方法?
对象相等 等价于 equals方法的结果相等。hashcode相等是对象相等的必要条件。
重写equals方法:原始的equals方法只是比较基本类型的数值和引用对象指向的地址是否相等,我们需要比较内容是否相等,所以需要重新equals。
但是如果只重写equals方法,每次都需要比较地址、类型、内容这样效率就很低。所以需要一个高效的先行比较方法。
重写hashcode方法:
1、在向set集合这种不重复的集合中插入数据的时候,如果只使用equals来遍历判断效率很低,我们需要根据对象自身计算一个hashcode来计算出它在set集合中的索引位置,便于我们比较。
2、原始的hashcode方法是通过对象的地址来计算hashcode值,会导致equals相等的对象hashcode不相等,所以需要重写来保证数据的一致性。
所以我们判断对象是否相等的时候会先用必要条件hashcode相等,然后再用充要条件判断。
常见的hashcode方法:
String计算hashcode:首先判断成员变量hash是否为0,如果为0,且String长度不为0则计算hashcode。
判断是latin1编码还是UTF16编码,调用的hashcode方法不同。
字节数组的hashcode:将数组长度右移一位(相当于除以2,一位UTF16存储字节占2字节)。然后for循环(数组长度一半)里计算h=31h+getChar(两个字节转化为一个字符)。
为什么是31:第一、31是素数,可以减少哈希碰撞;第二、31i可以通过位运算实现:(i<<5)-1
Object类型数组的hashcode:和字节数组类似。先判断是否为null,是,则返回0;再遍历对象
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
element的hashcode:它是一个本地方法,具体就是根据对象的地址值计算hashcode。
4、java异常
答:受检异常 & 非受检异常
受检异常:方法声明的时候证实了可能会有异常,代码编译的过程中必须显式处理否则编译报错,比如捕捉或者向上抛出。
例如:除了运行时异常和派生的子类、Error,其余基本都是。
非受检异常:一般是运行时异常类和他的子类,是代码逻辑问题,不用显示捕捉。
java异常家族:
Throwable(最顶级的异常类):包括Error和Exception
Error:系统或者硬件级别的错误,没法处理,只能退出程序。包括:虚拟机错误、线程死锁
常见的Error:OutOfMemoryError(爆内存)、StackOverFlowError、NoClassDefFoundError(编译器存在,但是运行期找不到类,例如类路径缺失,启动时漏掉了该类的JAR和目录,或者打包时没把依赖打进去,或者打进去之后被同名文件覆盖了)、InternalError(线程调度异常,属于JVM内部异常)、AssertionError(断言异常)
Exception:包括受检异常和非受检异常(运行时异常)
受检异常:文件异常、SQL异常、IOException(IO异常),编译期间必须显式处理。
非受检异常:主要是运行时异常及其子类,运行时异常不用throws,包括:数组索引越界、空指针、算数异常、唯一索引约束重复异常(插入数据时发生,也可做幂等)、类型转换异常、非法参数异常(IllegalArgumentException,例如枚举类使用valueOf()对参数进行校验)
try catch:运行时异常不用throws必须要catch,catch的顺序是先catch子类
异常处理模式:向上抛,抛给调用者、try catch,在catch代码块里处理。
5、枚举的特点、好处
特点:表示一组常量
好处:
枚举类的valueOf()方法可以校验参数
每个常量都可以定义一个方法,相比普通方法更灵活
天生是一个单例
枚举类是线程安全的,因为枚举类中的字段反编译后都是static final修饰的,而static字段在类加载的过程中就被初始化,且loadclass方法是线程安全的。
6、值传递、引用传递||深拷贝、浅拷贝
答:
值传递:调用函数的时候复制了一份值传递进去,不会影响原来的值。
引用传递:传递是对象引用,会改变原来的对象属性。
浅拷贝:创建一个新对象,对象属性中基本数据类型会拷贝值,引用数据类型会拷贝引用。
深拷贝:与浅拷贝不同的是,引用数据类型指向的对象也会进一步拷贝一个新的对象。
7、UUID
答:生成一个128位的二进制数,对同一时空中所有机器都唯一。
java用的是V3、V4
namespaceUUID V3:基于名称和名称空间生成MD5,保证同一名称空间不同名称不同,同一名称空间相同名称一定相同。
randomUUID V4:基于随机数的UUID
缺点:很长、可读性差。
(18)泛型
答:泛型就是将参数的类型进行参数化,调用方法需要传入具体的参数类
型。泛型是通过类型擦除实现的。所以泛型是工作在编译阶段的。
泛型擦除:在编译期间尽可能检查处错误,在编译的时候会把泛型给去除,生成的字节码文件里面没有泛型信息。
好处:
提升参数的复用性:List
安全:Object类型转换是在运行时进行才检查的,如果转换错误程序直接挂了,泛型可以在编译的时候起到一个约束作用
8、反射
获取class对象的方法:
//通过具体的类去拿class文件
Class A= a.class;
//通过全路径
Class A = Class.forName("cn.java.a");
//通过对象实例去拿
Class A = a.getClass();
//通过类加载器拿,但是这样拿到的class不会初始化,静态资源不会执行
ClassLoader.getSystemClassLoader.loadClass("cn.java.a");
反射就是通过class对象获取类的属性、方法与构造函数:
常用方法:
.newInstance();
.getConstructors();
.getDeclearedConstructors();
getFiled();
getFileds();
getDeclearedFields();
getMethod();
getDeclaredMethod();
// 如果对私有属性处理就需要设置可达性为true
.setAccessible(true);
package cn.javaguide;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {
/**
* 获取 TargetObject 类的 Class 对象并且创建 TargetObject 类实例
*/
<?> targetClass = Class.forName("cn.javaguide.TargetObject");TargetObject targetObject = (TargetObject) targetClass.newInstance();/*** 获取 TargetObject 类中定义的所有方法*/Method[] methods = targetClass.getDeclaredMethods();for (Method method : methods) {System.out.println(method.getName());}/*** 获取指定方法并调用*/Method publicMethod = targetClass.getDeclaredMethod("publicMethod",String.class);publicMethod.invoke(targetObject, "JavaGuide");/*** 获取指定参数并对参数进行修改*/Field field = targetClass.getDeclaredField("value");//为了对类中的参数进行修改我们取消安全检查field.setAccessible(true);field.set(targetObject, "JavaGuide");/*** 调用 private 方法*/Method privateMethod = targetClass.getDeclaredMethod("privateMethod");//为了调用private方法我们取消安全检查privateMethod.setAccessible(true);privateMethod.invoke(targetObject);
}
}
答:反射就是通过Class对象拿到对应类的所有信息,包括类名、成员变量、成员方法、构造函数。反射是工作在运行时阶段的
反射会导致程序变慢:
1、反射涉及到动态解析,无法执行虚拟机优化、
2、反射的时候会将对象包装到Object[],要使用的时候又要拆包,转化成真实对象,对象一多就会GC,GC会导致程序变慢
3、调用方法的时候需要在方法数组中遍历寻找,还要检查可见性,比较慢。
反射会破坏单例:
单例主要是将构造函数私有化,不让外部访问构造函数去构造多个实例。
但是反射可以拿到构造方法,并且修改权限为可访问,然后拿构造方法创建实例。
可以避免这种情况,方法就是构造函数里面判断单例是否不为空(双重判定-DCL),不为空则说明已经存在了,就可以抛异常。
相同的操作还有反序列化
9、注解
注解是一个特殊接口,注解的实现逻辑在注解处理器完成。
定义一个注解:
priority和description是注解的属性,定义的时候可以给默认值。
// 注解的作用目标:方法
@Target(ElementType.METHOD)
// 注解的保留策略:运行时可见
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
// 注解元素,默认值为 1
int priority() default 1;
// 注解元素,默认值为空字符串
String description() default "";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface a{
int n() default 1;
int b() default 2;
}
注解的使用:
注解的属性一般是为方法提供了额外的元数据信息,可以在编译时或者运行时被读取。
例如@order注解就可以在属性值里面给整数来规定方法的执行顺序。
import java.lang.reflect.Method;
public class AnnotationProcessor {
public static void main(String[] args) throws NoSuchMethodException {
MyClass myClass = new MyClass();
// 获取 importantMethod 方法Method importantMethod = MyClass.class.getMethod("importantMethod");// 检查方法上是否存在 MyAnnotation 注解if (importantMethod.isAnnotationPresent(MyAnnotation.class)) {// 获取注解实例MyAnnotation annotation = importantMethod.getAnnotation(MyAnnotation.class);System.out.println("方法名: " + importantMethod.getName());System.out.println("优先级: " + annotation.priority());System.out.println("描述: " + annotation.description());// 调用方法try {importantMethod.invoke(myClass);} catch (Exception e) {e.printStackTrace();}}// 获取 normalMethod 方法Method normalMethod = MyClass.class.getMethod("normalMethod");if (normalMethod.isAnnotationPresent(MyAnnotation.class)) {MyAnnotation annotation = normalMethod.getAnnotation(MyAnnotation.class);System.out.println("方法名: " + normalMethod.getName());System.out.println("优先级: " + annotation.priority());System.out.println("描述: " + annotation.description());try {normalMethod.invoke(myClass);} catch (Exception e) {e.printStackTrace();}}
}
}
10、@Value注解进行属性注入
@value注解可以根据属性占位符里的变量名称从配置文件里面读取属性值并注入。
可以注入字符串、数字、布尔值、数组、列表、Map
@value(${app.name}),属性占位符是配置文件中的值。
@value("#{'${app.name}'.split(',')}")
public List
@value("#{'${app.name}'}")
Map<String,String> map;
app={key1:value1,key2:value2};
11、 类的加载过程:
类的加载过程:
加载->链接->初始化
加载:通过类加载器加载类的二进制字节流byte[]到JVM堆内存中生成Class对象。
验证:文件格式验证、元数据验证、字节码验证、符号引用验证
准备:为静态变量分配内存
解析:将符号引用(一组字面量,用符号表示引用对象,一个java文件编译成class文件的时候类的地址用符号引用替代)转换为直接引用(目标对象的地址或者间接指向对象地址的一个句柄),实际上有一部分工作留到运行期进行解析(多态的解析)
初始化:执行clinit()方法,它是线程安全的。为静态变量赋初始值。
类加载器:
启动类加载器->标准拓展类加载器->应用程序类加载器。+自定义类加载器
BootStrap ClassLoader
Extension Classloader
Application ClassLoader
User ClassLoader
它们是层级关系,但不是继承关系
启动类加载器:加载java_home/lib下的jar包
标准拓展类加载器:加载java_home/lib/ext/下 的jar包
应用程序类加载器:加载系统路径下的类库
双亲委派机制:
一个类加载器收到加载类的请求,首先不会自己加载,先看有没有被加载过,如果没有则委派给它的上一级加载器加载,上一级加载器再委派给上一级,只有上级加载器加载不了才返给下级加载器加载。
任何一个类加载任务到来,先检查是否被加载(findloadclass()方法),如果没有就查看parent是否为空,如果不为空就调用parent的loadclass()方法来加载类。
好处:
保证类的唯一性,而且不被重复加载(因为加载的时候会看是否加载过)
保证程序安全性,不会被篡改。
如何打破:
自己写一个类继承ClassLoader并重写loadclass方法,例如TomCat。如果类是自动注入的,则需要在SpringApplication.run() 方法之前将Thread.ContextClassLoader中的类加载器设置为自定义的加载器,在自定义的类加载器中,if判断待加载的类非null,且类名称是目标类的名称,然后先自己加载,如果不是则使用parent上级加载器进行加载。
类加载器:
JDK1.8:
JDK1.9之后,拓展类加载器被重命名为平台类加载器PlatformClassLoader,负责加载JDK平台本身的类库,位于JDK/lib目录下
12、权修饰符
default是给本包访问的,protected是给子类访问的
子类和父类可能在同一个包也可能不在同一个包,所以子类的范围更大。
13、Tomcat
启动流程:init、load、start
最大线程数:200,最小空闲线程数:10
Tomcat的作用:
作为Web服务器:可以处理静态资源(HTML、CSS等静态资源),负责字节流与HTTPRequest对象之间的转化。
作为Servelet容器:管理Servelet的生命周期
客户端发起请求:根据URL构建HTTP请求,通过TCP/IP协议发送给对应的服务器的端口(例如Tomcat)
Tomcat接收请求:连接器Conector接收客户端的HTTP请求,并将字节流转化成HTTPServeletRequest对象(包含请求头、请求体)。
请求的映射与转发:Servelet容器捕获请求之后,传递给DispatcherServelet,DispatcherServelet根据路由规则转发给对应的Controller。
Controller处理:执行业务逻辑,将响应结果返回给DispatcherServelet,然后DispatcherServelet将响应结果写入HTTPServeletRequest对象。
Tomcat封装并返回:然后将HTTPServeletRequest封装成HTTP响应返回。
Tomcat打破双亲委派机制:
因为Tomcat可以运行多个应用,不同应用依赖包的版本可能不一样,如果使用双亲委派机制无法加载同一个类的不同版本,所以为了具有隔离性,每一个应用都会有一个自己的WebAppClassLoader加载类,对于版本相同的类,就用SharedClassLoader加载。
delegate=false就会打破双亲委派机制
14、Comparator比较器:
list.sort((a,b)->a-b) 相当于
list.sort(new Comparator
public int compare(Integer o1,Integer o2){
return o1-o2;
})
Comaparable接口:
可以自定义类,然后实现comparable接口,重写compareTo方法
需要实现compareTo方法,Integer、Double、String都实现了comparable接口,可以直接调用conpareTo方法:返回值小于0,则当前对象小于传入的对象。
return num1.conpareTo(num2)
15、BIO、AIO、NIO模型
BIO:同步阻塞模型。
NIO:同步非阻塞模型,线程发起请求后直接返回,无需阻塞,后续轮询IO缓冲区即可。
AIO:异步非阻塞模型,与NIO的区别在于无需轮询,而是IO准备好执行回调函数通知。
同步非阻塞的IO模型,核心三要素:通道、缓冲区、Selector
初始化
○ 创建Selector对象,它是 NIO 的核心组件,用于监听多个Channel上的事件。例如:Selector selector = Selector.open();
○ 创建Channel通道,常见的通道类型有SocketChannel(用于 TCP 网络通信)、ServerSocketChannel(用于监听 TCP 连接)等。以SocketChannel为例:SocketChannel channel = SocketChannel.open();
○ 创建Buffer缓冲区,根据数据类型的不同,有ByteBuffer、CharBuffer等多种类型。如:ByteBuffer buffer = ByteBuffer.allocate(1024);
注册通道到选择
○ 将Channel配置为非阻塞模式,因为 NIO 是基于非阻塞的 I/O 操作。对于SocketChannel:channel.configureBlocking(false);
○ 将Channel注册到Selector上,并指定需要监听的事件。事件类型包括连接事件(SelectionKey.OP_CONNECT)、读事件(SelectionKey.OP_READ)、写事件(SelectionKey.OP_WRITE)等。例如,注册读事件:channel.register(selector, SelectionKey.OP_READ);
监听事件
使用Selector的select()方法监听注册通道上的事件。该方法会阻塞,直到有事件发生,返回发生事件的通道数量。例如:int readyChannels = selector.select();
当select()方法返回大于 0 的值时,表示有事件发生,可以通过selector.selectedKeys()获取发生事件的SelectionKey集合。Set
处理事件
○ 遍历selectedKeys集合,针对不同类型的事件进行相应处理。
○ 读事件处理:当通道有可读数据时,将数据从Channel读取到Buffer中。例如:文件读取的时候会先把数据放到buffer里面,然后从buffer到channel。再或者网络通信的时候TCP对应的socketchannel也是先把数据从通道放到buffer里,然后在由线程读取,进行处理。
○ int bytesRead = channel.read(buffer);,读取数据后,可能需要对Buffer中的数据进行处理,如解析、转换等。处理完数据后,需要根据情况清空或调整Buffer的状态,以便下次使用,如buffer.clear();或buffer.flip();
○ 写事件处理:如果是写事件,将Buffer中的数据写入到Channel。例如:int bytesWritten = channel.write(buffer);,同样在写入后,根据需求调整Buffer状态。
○ 连接事件处理:当连接事件发生时(OP_CONNECT),完成连接操作或处理连接相关逻辑。比如在客户端连接服务器时,检查连接是否成功:if (channel.isConnectionPending()) { channel.finishConnect(); }
关闭资源
○ 在操作完成后,需要关闭Channel和Selector等资源,以释放系统资源。例如:channel.close(); selector.close();
16、单例和多例
单例与多例的区别是一个类的实例数量。但是他们都不对外提供构造方法。通过@Scope注解控制对象单例(Singleton)/多例(Prototype)。
使用规则:当对象含有可变状态时(实际应用中状态可变),则使用多例,否则单例。
单例应用场景:数据库、redis的连接对象、IOC容器。
多例应用场景:线程池创建线程、或者数据库的连接池。
使用单例是基于享元设计模式,避免每次请求都创建一个新对象,减少内存占用;使用多例是避免出现线程安全问题。
17、重写和重载
重载:编译期概念,它指的是多个方法的名称相同,但参数列表、返回值和权限修饰符都可以不同。编译器通过参数列表来判定重载的具体方法。
重写:运行期概念,它指的是子类继承父类重写父类的方法,方法名和参数列表必须相同,权限修饰符不能比父类的严格,返回值类型可以是父类返回值类型,也可以是其子类。
18、java对象头的组成
19、Java对象头
我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
20、Java集合框架
单列集合:Collection接口:List接口,ArrayList、LinkedList、Vector;Set接口,HashSet、TreeSet;
双列集合:Map接口:HashMap,Hash Table,ConcurrentHashMap
21、接口与抽象类的区别:
方法定义:
接口里面只有抽象方法,java 8之后有默认方法,抽象类里面都有。抽象类里面可以有普通成员变量,但是接口里面只能有常量。
实例化的角度:
二者都不能实例化,但是抽象类里面有构造方法,但是不能使用构造方法实例化,构造方法一般用于初始化变量。
设计模式的角度:
接口是一种规范,我们对外提供的是接口,也称为面向接口编程。抽象类是抽取出多个实现类的共用逻辑,复用代码,例如模板方法