目录
一、Java基础的知识点概要
1.Java基础知识点概要
数据类型
运算符
分支结构
循环结构
面向对象(封装、继承、多态)
抽象类、接口
修饰符(static、final、public。。。)
内部类
泛型
异常
集合
反射
动态代理
序列化
二、重要知识点
1.异常
异常分类
Exception 和 Error
Exception 和 Error 都是继承了 Throwable 类。
Exception 应该被捕获,进行相应处理。Exception 又分为可检查(checked)异常和不检查(unchecked)异常。可检查异常(IOException、SQLException)在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是RuntimeException,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获。
Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。尽量不要捕获。
附加知识点,NoClassDefFoundError和ClassNotFoundException区别:NoClassDefFoundError 是个Error,是指一个class在编译时存在,在运行时找不到了class文件了;ClassNotFoundException 是个Exception,是使用类似Class.foName()等方法时的checked exception。
实战技巧
1.尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常
2.不要e.printStackTrace();而是logger.info(e.getMessage())
3.建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码(因为try-catch 代码段会产生额外的性能开销)
2.集合
集合框架图
Collection接口的子接口包括:Set、List、Queue
Set的实现类:HashSet、TreeSet、LinkedHashSet
List的实现类:ArrayList、LinkedList、Stack、Vector
Queue的实现类:ArrayBlockingQueue、LinkedBlockingQueue
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap、Properties
fail-fast机制和fail-safe机制
fail-fast(快速失败):当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出ConcurrentModificationException异常,java.util下都是快速失败。不能在多线程下发生并发修改(迭代过程中被修改,快速失败和安全失败是对迭代器而言的)。
fail-safe(安全失败):在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在java.util.concurrent下都是安全失败。
List
Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择。扩容时会提高 1 倍。
ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的。 ArrayList 扩容时增加 50%。适合查询、修改。
ArrayList为什么线程不安全:
1.多线程进行add操作时可能会导致elementData数组越界;
2.elementData[size++] = e中size++不是原子操作。
如需要换用CopyOnWriteArrayList或者Collections.synchronizedList(list)。
LinkedList 是双向链表,所以它不需要扩容,它也不是线程安全的。适合插入、删除。
Set
TreeSet 支持有序访问,但add、delete、contains相对低效(O(log(n)))。所有方法都是基于TreeMap实现的,底层是红黑树。与HashSet不同的是其不需要重写hashCode()和equals()方法。
HashSet 利用哈希算法,如果哈希散列正常,可以提供时间复杂度O(1)的add、delete、contains操作。
LinkedHashSet 内部构建了一个记录插入顺序的双向链表,也可以提供时间复杂度O(1)的add、delete、contains操作。
Map
Hashtable早期 Java 类库提供的一个哈希表实现,本身是线程安全的(所有方法用synchronized修饰),不支持 null 键和null值。初始容量为11,扩容capacity*2+1。
HashMap线程不安全,支持 null 键和null值。HashMap 进行 put 或者 get 操作,时间复杂度O(1)~O(n)。
TreeMap基于红黑树的一种提供顺序访问的 Map,它的get、put、remove 之类操作都是 O(logn)的时间复杂度,默认是按键值的升序排序(自然顺序),也可以指定排序的比较器Comparator。不支持 null 键和但支持null值。
LinkedHashMap,散列表+双向链表,散列表部分和HashMap一致,而双向链表部分则是来一个就插到尾部,这样就保证了保持插入顺序。继承了HashMap类,大部分方法直接沿用,但put方法虽然未复写,put方法里有一个方法是构造一个新节点newNode,这里LinkedHashMap重写了。
LinkedHashMap有一个构造方法可以传递accessOrder=true,可以按照访问顺序排序,最近常使用的排到最后,最近不常使用的排到最前,size()撑爆时删除最近不常使用的,实现LRU算法。利用LinkedHashMap实现LRU算法的代码如下:
Map<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(0, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > maxSize;
}
};
HashMap
数组:存储区间是连续,空间复杂也很大,时间复杂为O(1)。随机读取效率高,插入和删效率低。
链表:区间离散,空间复杂度小,时间复杂度O(n)。插入删除效率高。
哈希表:数组+链表,以实现查询效率高和插入删除效率也高。时间复杂度O(1)~O(n)。
红黑树:时间复杂度O(logn)。
基本原理:JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
put(k,v)实现原理
1.如果table为空或者长度为0,那么使用resize()方法扩容
2.计算插入存储的数组下标i
3.如果数组为空,即不存在Hash冲突,则直接插入数组;
如果数组不为空,即发生Hash冲突:
a.检查数组的第一个Node,利用equals比较key,相等直接覆盖;否则转b
b.继续判断,需要插入的数据结构是红黑树还是链表,如果是红黑树,则直接插入;否则转c
c.需要插入的数据结构是链表,则利用equals比较key,若存在则覆盖;否则插在链表尾部。插入完成后判断如果链表长度达到8,则存储结构改为红黑树(treeifyBin方法改红黑树前会先判断hashMap的长度,如小于64只进行resize,大于64才改红黑树)
4.插入成功后,判断下是否需要resize。
get(k)实现原理
1.计算插入存储的数组下标i
2.检查数组的第一个Node,利用equals比较key,是要找的就直接返回;
3.如果不是则判定下该table[i]是红黑树还是链表,然后到对应的数据结构下利用equals比较key去查找。
数组下标计算
i = (n-1) & hash,hash的计算是return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16;
为什么计算hash要右移16位:如果直接使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,与自己右移16位进行异或(当数组长度为2的幂次方时,h&(length-1)等价于h%length),进一步降低hash碰撞的概率。
总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
1. 使用链表;
2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快。
扩容机制resize()
创建HashMap时,如果不指明初始大小,默认大小为16(即数组大小16),如果HashMap中的元素个数达到(负载因子 x 容量),则重新调整HashMap大小变为原来2倍,扩容很耗时。
大致步骤:
1.先确定是哪种情况扩容:a.初始化哈希表;b.当前数组容量过小,需要扩容
2.把每一个bucket都移动到新的bucket中去,这是扩容的一个主要开销来源。
如果key是对象类型,就必须重写hashcode()和equals()的原因:
1.重写equals()因为比较元素时要用;
2.如果你重写了equals(),而保留hashCode()的实现不变,那么很可能某两个对象明明是“相等”,而hashCode()却不一样。所以Java规定重写equals(),必须重写hashcode()。前者相等后者必须相等。
HashMap线程安全问题
JDK1.7的扩容时循环链问题:并发环境下,需要扩容时候出现循环链表,通过get获取值造成死循环,CPU飙升。(扩容时数据会发生数据迁移transfer(),迁移的过程就是一个rehash()的过程,多个线程同时操作导致Entry的next被并发修改,就有可能会形成循环链表)。JDK1.8引入了红黑树优化数组链表,同时改成了尾插(新增元素在链表尾部,JDK1.7是头插),理论上是不会有环了。另说明,JDK1.8中没有transfer()方法,而是在resize()中完成了数据迁移。
JDK1.7的扩容时数据丢失:也是因为transfer(),并发时next被提前置为null等原因。
JDK1.8在并发执行put操作时会发生数据覆盖:因为并发赋值时数据会被覆盖。
ConcurrentHashMap
整体结构
JDK1.7:ReentrantLock+Segment+HashEntry
JDK1.8:Node数组+链表+红黑树的数据结构来实现,与HashMap结构相似,采用synchronized+CAS保证线程安全
JDK1.8总体优化:
1.取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2.JDK1.7采用Segment的分段锁机制实现线程安全,其中Segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。直接锁bucket,降低锁的粒度。
3.引入了红黑树结构,从原来的遍历链表O(n),变成遍历红黑树O(logn)。总体复杂度O(1)~O(logn)。
put()
JDK1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。
JDK1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似JDK1.7)
get()
两者基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁。
resize()
JDK1.7:跟HashMap步骤一样,只不过是搬到单线程中执行,避免了HashMap在1.7中扩容时死循环的问题,保证线程安全。
JDK1.8:支持并发扩容,与JDK1.8的HashMap一样扩容时由头插改为尾插(为了避免循环链问题),迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。
size()
JDK1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的Segment求和。
JDK1.8:CAS修改baseCount值,失败的线程会继续执行方法体中的逻辑,使用CounterCell记录元素个数的变化。累加baseCount和CounterCell数组中的数量,即可得到元素的总个数。
TreeMap
红黑树
红黑树是一种特定类型的二叉树,它维持大致上的平衡,适合频繁的插入和删除,可以达到O(logn)的时间复杂度。
put()操作
根据二叉查找树的特性,遍历找到新节点合适的插入位置,最后通过fixAfterInsertion(e)方法来进行自平衡处理(进行重新着色和左旋右旋操作,保证红黑树在进行插入节点之后,仍然是一颗红黑树)。(左旋:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点)
get()操作
get方法是通过二分查找的思想。
remove()操作
先是找到这个节点,直接调用了getEntry(Object key),通过deleteEntry(p)进行删除操作,最后通过fixAfterDeletion进行自平衡操作。
3.反射机制
反射是什么
JAVA反射机制是在运行状态中:
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java的反射机制。
程序中对象的类型一般都是在编译期就确定下来的,而当我们的程序在运行时,可能需要动态的加载一些类,这些类因为之前用不到,所以没有加载到jvm,这时,使用Java反射机制可以在运行期动态的创建对象并调用其属性,它是在运行时根据需要才加载。
反射的原理
下图是类的正常加载过程,反射原理与class对象:
Class对象的由来是将.class文件读入内存,并为之创建一个Class对象。
获取反射入口(class对象)的三种方法
1.Class.forName("全类名")
2.类名.class
3.对象.getClass()
通过反射来生成对象的两种方法
1.Class对象的newInstance()方法
Class<?> c = String.class;
Object str = c.newInstance();
2.先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法
//获取String的Class对象
Class<?> str = String.class;
//通过Class对象获取指定的Constructor构造器对象
Constructor constructor=c.getConstructor(String.class);
//根据构造器创建实例:
Object obj = constructor.newInstance(“hello reflection”);
反射的用途或场景
1.根据反射入口对象(class)获取类的各种信息(所有public方法、构造方法等)
2.通过反射获取对象的实例,并操作对象(调用对象的方法)
3.当我们在使用IDE,比如Ecplise时,当我们输入一个对象或者类,并想调用他的属性和方法是,一按点号,编译器就会自动列出他的属性或者方法,这里就是用到反射。
4.反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean、Spring IOC),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。
反射的缺点
1.由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。
2.另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
4.动态代理
代理模式
代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托了(真实对象)预处理消息、过滤消息、传递消息给委托类,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理。
通过代理静默地解决一些业务无关的问题,比如远程、安全、事务、日志、资源关闭。
静态代理:事先写好代理类,可以手工编写,也可以用工具生成。在程序运行之前,代理类的.class文件就已经生成。缺点是每个业务类都要对应一个代理类,非常不灵活。
动态代理:代码运行期间加载被代理的类这就是动态代理。缺点是生成代理对象和调用代理方法都要额外花费时间。
Java 反射机制的常见应用:动态代理(AOP、RPC)、提供第三方开发者扩展能力(Servlet容器,JDBC连接)、第三方组件创建对象(DI)。
JDK动态代理
JDK动态代理:基于Java反射机制实现,必须要是接口的实现类才能用这种办法生成代理对象。新版本也开始结合ASM机制。
使用方法
1.创建一个接口
public interface Subject {
void hello(String param);
}
2.实现接口
public class SubjectImpl implements Subject {
@Override
public void hello(String param) {
System.out.println("hello " + param);
}
}
3.创建SubjectImpl的代理类
public class SubjectProxy implements InvocationHandler {
private Object target;
public SubjectProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("--------------begin-------------");
Object invoke = method.invoke(target, args);
System.out.println("--------------end-------------");
return invoke;
}
}
4.编写代理类实际的调用,利用Proxy类创建代理之后的Subject类。
public class Main {
public static void main(String[] args) {
Subject subject = new SubjectImpl();
InvocationHandler subjectProxy = new SubjectProxy(subject);
Subject proxyInstance = (Subject) Proxy.newProxyInstance(subjectProxy.getClass().getClassLoader(), subject.getClass().getInterfaces(), subjectProxy);
proxyInstance.hello("world");
}
}
原理解析
调用Proxy.newProxyInstance生成代理类的实现类:
1.调用getProxyClass0寻找或生成指定代理类
2.缓存调用ProxyClassFactory生成代理类,proxy class的生成最终调用ProxyClassFactory的apply方法
3.ProxyGenerator.generateProxyClass使用生成的代理类的名称、接口、访问标志,生成proxyClassFile字节码
4.生成字节码之后利用反射生成实例
CGLIB动态代理
CGLIB动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。(ASM 是一个 Java 字节码操控框架。)
使用方法
1.引入CGLIB的jar包
2.创建代理类
public class CGsubject {
public void sayHello(){
System.out.println("hello world");
}
}
3.实现MethodInterceptor接口,对方法进行拦截处理。
public class HelloInterceptor implements MethodInterceptor{
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("begin time -----> "+ System.currentTimeMillis());
Object o1 = methodProxy.invokeSuper(o, objects);
System.out.println("end time -----> "+ System.currentTimeMillis());
return o1;
}
}
4.创建被代理类,利用Enhancer来生产被代理类,这样可以拦截方法
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CGsubject.class);
enhancer.setCallback(new HelloInterceptor());
CGsubject cGsubject = (CGsubject) enhancer.create();
cGsubject.sayHello();
}
}
原理解析
Cglib是一个优秀的动态代理框架,它的底层使用ASM在内存中动态的生成被代理类的子类,使用CGLIB即使代理类没有实现任何接口也可以实现动态代理功能。CGLIB具有简单易用,它的运行速度要远远快于JDK的Proxy动态代理。
CGLIB的核心类:
net.sf.cglib.proxy.Enhancer – 主要的增强类
net.sf.cglib.proxy.MethodInterceptor – 主要的方法拦截类,它是Callback接口的子接口,需要用户实现
net.sf.cglib.proxy.MethodProxy – JDK的java.lang.reflect.Method类的代理类,可以方便的实现对源对象方法的调用
5.序列化
什么是序列化与反序列化
Java 序列化是指把 Java 对象转换为字节序列的过程;
Java 反序列化是指把字节序列恢复为 Java 对象的过程。
为什么要序列化与反序列化
1.把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2.在网络上传送对象的字节序列。
场景:
1.Web 容器就会把一些 Session 或者图片视频等先序列化,让他们离开内存空间,序列化到硬盘中,当需要调用时(用户再次访问),再把保存在硬盘中的对象还原到内存中。
2.两个进程进行远程通信时,彼此可以发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。
序列化方法
Java原生序列化
实现Serializable接口(隐式序列化):使用ObjectInputStream的writeObject()和ObjectOutputStream的readObject()就能实现反序列化和序列化。
缺点:三种序列化方法中最低效的一种。
Json序列化
使用Alibaba的FastJSON。
优点:有极快的性能,具备可读性 ,不需要对要序列化的类做特殊处理。
JSON.toJSONString(user);
JSON.parseObject(text, User.class);
Google 的protobuf
原始的ProtoBuff需要自己写.proto文件,通过编译器将其转换为java文件,显得比较繁琐。百度研发的jprotobuf框架将Google原始的protobuf进行了封装,对其进行简化,仅提供序列化和反序列化方法。
优点:跨语言;序列化后数据占用空间比JSON小。三种方法中效率最高的一种。
缺点:需要配置或者标注。但jprotobuf框架已解决。
@Protobuf(fieldType = FieldType.INT32, required = false, order = 1)
private Integer userId;
Codec<User> studentClassCodec = ProtobufProxy.create(User.class, false);
studentClassCodec.encode(u2);
studentClassCodec.decode(bytes);
三、其他Java生态相关
1.maven相关
maven依赖原则
maven依赖原则总结起来就两条:路径最短,声明顺序其次。
路径最短优先
如 E->F->D2 比 A->B->C->D1 路径短。Maven 面对 D1 和 D2 时,会默认选择最短路径的那个 jar 包,即 D2。
最先声明优先
如果路径一样的话,如: A->B->C1, E->F->C2 ,两个依赖路径长度都是 2,那么就选择最先声明。
scope的使用
compile
默认的scope,表示 dependency 都可以在生命周期中使用。而且,这些dependencies 会传递到依赖的项目中。适用于所有阶段,会随着项目一起发布。
provided
跟compile相似,但是表明了dependency 由JDK或者容器提供,例如Servlet AP和一些Java EE APIs。这个scope 只能作用在编译和测试时,同时没有传递性。
一般通过添加<scope>provided</scope>,表明该包只在编译和测试的时候用,减少运行时冲突的可能。