JAVA
什么是编译期常量
必须是编译时期就能得到确定的值,赋值公式没有变量参与进来,变量必须在运行期间变成实际值才能判断,所以不是编译期间变量
String的不可变更
因为内部的char数组用final修饰而且private了,所以修改不了,如果要修改只能用反射去修改char数组
泛型擦除
编译的时候会进行泛型擦除,会把泛型T 擦除成Objet , E extends xxx 擦除成xxx
为什么要用泛型,直接object?
1.强转麻烦而且代码可读性低
2.使用泛型,编译器就会进行检测,不用等到运行期
3.泛型可以自限定类型,控制变量类型
为什么在实现的时候没有使用泛型,编译器会做桥方法?
为了保持多态,比如父类有T 但是子类实际化成了Integer, 那么擦除的时候,实际T是会变成objcet, 所以没有重写
这时候编译器会做个桥方法,把object方法内部调用同名Integer进行转发
不能使用statc修饰泛型
通配符和泛型区别
?通配符是为了具体的类型,泛型会擦除成object,但是?extend xxx会是通配成具体的xxx子类
List<?> list =new ArrayList 是不能存入任何值的,因为这个没有类型所以会导致数据异常
通配有 extend 和super 两个边界, 一个是子类,一个是父类
内部类
成员内部类
静态内部类
局部内部类
匿名内部类
静态内部类和非静态内部类的区别
1.创建:静态内部类是通过外部类名.内部类创建,非静态内部类是通过创建了的外部类的对象进行创建
2.生命周期:静态内部类实例是独立生存,非静态内部类实例依赖外部类的对象,当外部对象死亡,内部类对象死亡
3.权限:静态内部类不持有外部类实例引用,不能调用非静态参数方法。静态内部类访问权限和其他类一样,不可以访问private
但是非静态内部类可以访问所有的数据和参数
泛型(后期)
Class.forName和ClassLoader的区别
SpringAop和rpc怎么使用动态代理?
SpringAop的本质是根据切点去增加前后拦截,然后创建代理类。
rpc也是进行了封装了http或者其他协议的调用,将调用代理成本项目包可以使用的对象和接口,实际上是远程请求和重新组装数据放到java对象中
时间复杂度
时间复杂度就是代码执行次数和数据规模参数n的变化
比如固定执行100次 那就是o(1)
如果根据n动态执行n次 就是O(n)
空间复杂度
针对空间随着n变化而变化
比如一个循环n次,循环内部没有申请额外空间,那就是o(1)。如果每次都申请1空间了,那就是O(n)
如果一个数组以n为申请内存大小,那么就是o(n)
java动态代理原理
利用反射的原理,同时动态创建一个需要的代理类实现接口,同时该代理实现里面有一个InvocationHandler参数,InvocationHandler里面注入实际对象,实际调用的时候是获取到调用的方法信息、代理信息、然后handler里面有真实对象去处理,同时利用反射和对象进行对象方法调用
如果是浮点数就用BigDecimal
Unsafe.loadFence()
实际是读内存屏障,作用是将禁止屏障前和屏障后的读取指令重排,并且将cpu多级缓存清空,下次从主存拿取最新的值
Unsafe可以直接操作内存,然后给内存重新进行写值,规定是只有在基础包里面可以用,一般简单的方法是通过反射调用
JAVA的SPI怎么实现的
java的SPI通过META-INF/service目录下的类名(需要实现的接口的全类名)和内容(实现类的全类名,多个就换行)去加载SPI指定的实现类,然后通过反射创建实现类转换成接口对象,返回实现对象list给上端调用方获取,调用方再实现选择调用那个实现的具体策略
ArrayList和vector区别
vector用sync锁住 ArrayList没有
ArrayList的操作时间复杂度
首端插入、删除:因为要移动后面的所有数据,所以是o(n)
终端插入、删除:不用移动,最多是扩容、平摊下来是o(1)
中间插入、删除:好的的时候是o(1),坏的时候是o(n),平摊下来是o(n)
ArrayList扩容
针对最小需要容量大小,默认初始为0,加入第一个元素的时候默认是10,如果超过现有的最大容量,在现有的基础上扩容1.5(位运算扩容,加0.5),如果还不够就用最小需要容量作为新容量
ArrayList是浅拷贝
Arrays.asList使用转数组到集合,所以修改数组就会影响,因为使用是内部类,只是包装一层
LinkList的操作时间复杂度
首段,终端插入、删除:因为底层是链表,所以只用o(1)
中间插入、删除:因为底层是链表,移动没有什么操作,但是查找需要o(n),所以是o(n)
Comparable和Comparator
Comparable用来继承的接口是要进行重写compareTo,如果是顺序集合会自动调用这个方法比较然后排序
Comparator是一个排序的自定义实现子类接口,使用Collentions.sort进行排序
Set的实现原理
特殊功能的标识接口
1.RandamAcess 这个接口标识是可以随机访问的集合,内部使用数组实现
FIFO
FIFO先进先出
集合的分类和内部细类区别
Collection: set(值唯一)、list(顺序不唯一)、queue(模仿栈、单进单出、特殊可以两边都进都出)
ArrayList底层是动态数组(有扩容机制),线程不安全,vector也是动态数组,通过sync保证线程安全。LinkList底层是链表,线程不安全。
数组是连续的内存块,所以扩容需要大块内存,链表是离散的,所以可以是多个小块内存。
set:HashSet(hashMap实现)、LinkHashSet(linkHashMap试下)、SortSet、TreeSet
Queue:Deque、ArrayDeque、priorityQueue(优先级优先的堆,可以通过Comparator定义),BlockingQueue
map:HashMap、LinkHashMap、SortMap、TreeMap
Map
HashMap循环效率
entrySet > Steam > keySet
HashMap的扩容机制
如果创建的时候指定了就是指定的大小最接近的最小2次幂大小,没指定默认16。
初始数据结构是数组加链表的方式,如果出现hash碰撞就再数组的头结点后以链表方式处理。如果hash碰撞超过了8但是数组长度没有达到64就进行数组扩容,如果数组长度达到了64就将链表转成红黑树,数组存储达到默认的0.75也进行数组扩容。
为什么HashMap的容量必须是2次幂
1.主要是map获取hash在map中应用的位置的时候要用数组的大小n去取余hash值,hash方法的本身是比较均匀分散的,所以使用去余也可以保证hash碰撞减少。同时因为当n是2次幂的时候 hash%n = hash&(n-1),位运算的速度要快些,所以使用2次幂
2.在扩容的时候,如果是翻倍的话用原值计算位置,那么就是hash&(2n-1),在二级制上其实容量2n就是n向左移动了一位,而2n-1就是n-1左移同时最低位给1,这样计算位置的时候要么是原位,要么是原位+n,这样扩容计算也快点
HashMap死循环(这是一个伪问题)
hashmap的1.7因为扩容是头插式,也就是扩容时候会新建一个节点链表,然后复制对应的值后,重新指定到数组,因为是头插,所以ABC这个链表就会变成CBA。而在多线程下,如果在扩容前插入线程定位到节点为T1=A,T1.next=B,然后休眠等待扩容,扩容完成后线程还定位在T1=A,T1.next=B,而B.next=A
ConcurrentHashMap本身的原子性
ConcurrentHashMap本身只针对提供的基础方法和内部包装好了的复合方法保证原子性(通过锁定node和cas),并不是保证外部自定义的操作符合原子性
ConcurrentHashMap的实现原理就是对首node进行锁
集合实现线程安全的方法
1.CopyOnWriteArrayList
使用sync和volatile sync锁定后复制数组(避免修改数组导致ArrayList的状态不一致报错)然后去添加到复制数组,最后将复制数组放到volatile修饰的原数组
2.ConcurrentHashMap(代替HashMap)
1.7使用分区的概念给写操作加锁,实现线程安全
1.8使用锁头结点的方式,当扩容和写操作的时候在锁头上加Sync锁,使用cas去更新,如果更新失败就重新执行一遍循环,写到下一位
TreeMap
TreeMap是有顺序,实现是通过红黑树实现,可以指定比较器,HashMap的key可以为null,TreeMap不行
集合不能直接在foreach中remove,而是要通过迭代器remove,不然迭代器的计数和外部计数不一致导致异常
迭代器有一个计算操作次数的计数器,而集合外部也有一个,只有内外两个保持一致才行,不然就会报错。
线程的理解
线程就像一条线,走过每个方法。其中局部创建的变量是线程安全的(单线程使用),成员变量是不安全的(多线程共用)。