一、Java基础部分
1、面向对象和面向切面的区别?
AOP(面向切面编程):通过预编译方式、运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,提高开发效率。
OOP(面向对象编程) 针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。但是也有它的缺点,最明显的就是关注点聚焦时,面向对象无法简单的解决这个问题,一个关注点是面向所有而不是单一的类,不受类的边界的约束,因此OOP无法将关注点聚焦来解决,只能分散到各个类中。 AOP(面向切面编程)则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。 AOP并不是与OOP对立的,而是为了弥补OOP的不足。OOP解决了竖向的问题,AOP则解决横向的问题。因为有了AOP我们的调试和监控就变得简单清晰。
参考:知乎-AOP与OOP有什么区别,谈谈AOP的原理是什么
2、线程池相关知识
线程池作用:
1、减少在创建和销毁线程上所花的时间以及系统资源的开销。
2、如果不使用线程池,可能造成系统创建大量线程。
线程池的种类:
- 可缓存线程池(newCachedThreadPool) 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- 指定工作线程数量的线程池(newFixedThreadPool) 创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
- 单线程线程池(newSingleThreadExecutor) 创建一个单线程化的Executor,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- 定时的线程池(newScheduledThreadPool) 创建一个定长的线程池,支持定时及周期性任务执行。
参考:JAVA线程池有几种类型?
线程池参数:
- corePoolSize:核心线程数,默认会一直保持在线程池中,即便是空闲也会保持。除非设置了allowCoreThreadTimeOut参数,设置此参数后,超时后会被销毁。
- maximumPoolSize:线程池中所允许的最大线程数。
- keepAliveTime:超过核心线程数的线程,如果在此参数指定的时间内还没有接收到新任务,则会被销毁。也就是说此参数是指定超过核心线程数的空闲线程的最大存活时间。
- unit:存活时间的单位。
- workQueue:用来保存通过execute(Runnable command)方法提交的未执行的任务。也就是在任务执行之前用来保存任务的队列。
- threadFactory:用来创建线程的工厂。
- handler:饱和拒绝策略。当不能创建新线程即已经达到了可创建线程的边界(maximumPoolSize)并且用来保存任务的队列(workQueue)已满的时候执行的策略。
工作队列:
1、ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。
3、SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
4、PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
拒绝策略:
1、Abort策略:丢弃任务并抛一个RejectedExecutionException异常。(默认拒绝策略)
2、CallerRuns策略:由调用线程(提交任务的线程)处理该任务。
3、DiscardPolicy策略:直接丢弃任务,不抛异常。
4、DiscardOldestPolicy策略:丢弃队列头中的第一个任务,重新提交被拒绝的任务。
参考:JDK1.8线程池参数
3、JDK1.8 JVM运行时数据区域划分
这里介绍的是JDK1.8 JVM运行时内存数据区域划分。1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
- 程序计数器: 线程私有,每个线程一块,指向当前线程正在执行的字节码代码的行号。如果当前线程执行的是native方法,则其值为null。
- Java虚拟机栈: 线程私有,生命周期与线程同进同退。每个Java方法在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。
- 本地方法栈: 线程私有,功能与Java虚拟机栈十分相同。区别在于,本地方法栈为虚拟机使用到的native方法服务。
- Java堆: 线程共享,是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在堆上进行分配。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。
- 元数据区: 线程共享,元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息,静态变量,常量等数据。运行时常量池存在于内存的元空间中。
参考:JDK1.8 JVM运行时数据区域划分
4、==和equals()的区别?
- ==:如果比较的是基本数据类型,则直接比较其存储的 “值”是否相等;如果比较的是引用类型的变量,则比较的是所指向的对象的地址。
- equles():如果没有重写equals()方法比较的是对象的地址,因为对Object来说对象没有什么属性可以比较,只能比较最底层的地址。而如果重写equals()方法时,该方法的对象因为是Object的子类,所以调用时会调用子类对象里面的方法。所以只有重写equals()方法后,两者比较的才是内容、或者说重写可以使自己定义比较的规则,不想按照地址去比较。
- 如何重写自己写的类中的equals()方法?
答:(1)判断要比较对象的是否为null,若是直接返回false,若不比较则可能会出现空指针异常(NullPointerException);
(2)判断是否在与自身比较(通过==比较地址),若是直接返回true;
(3)判断要比较的两个对象是否为同类,若是再进行接下来比较,若不是直接返回false。若不判断,则可能出现强转异常(classCastException);
(4)通过向下转型,比较两对象内容是否相等。 - equals和hashcode常规协定
1、两个对象用equals()比较返回true,那么两个对象的hashCode()方法必须返回相同的结果。
2、两个对象用equals()比较返回false,不要求hashCode()方法也一定返回不同的值,但是最好返回不同值,以提高哈希表性能。
3、重写equals()方法,必须重写hashCode()方法,以保证equals方法相等。
class Person{
private String name;
private int age;
public Person(String name,int age){
this.name=name;
this.age=age;
}
public String toString(){
return this.name+"今年"+this.age+"岁";
}
@Override
public int hashCode() {
int result = 17;
result = result * 31 + name.hashCode();
result = result * 31 + age.hashCode();
return result;
}
@Override
public boolean equals(Object obj){//Object类可接受任何类
if(obj==null){//判断是否为空,若不判断则会出先空指针异常(NullPointerException)
return false;
}
if(this==obj){//判断是否在与自身比较(通过比较地址),若是则直接返回true
return true;
}
if(!(obj instanceof Person)){//instanceof作用为判断其左边对象是否为右边对象的实例,此处为判断主方法中equals()方法括号中的对象是否为Person类
return false;
}
//到达此处时必定是同类但不同地址的对象在比较
Person per=(Person)obj;//向下转型,比较属性值
return this.name.equals(per.name)&&this.age==per.age;//判定属性内容是否相等(易错点)
}
}
5. GC算法
判断对象是否死亡:
引用计数法。无法解决对象之间相互循环引用的问题。
可达性分析法。可以作为GC Root的有:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象。
2、方法区中类静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
6、常量池相关
String s1 = new String("123");
此行代码创建了两个对象,在执行前会在常量池中创建一个"123"的对象,然后执行该行代码时new一个"123"的对象存放在堆区中;然后s1指向堆区中的对象;s1 = s1.intern();
把堆中s1对象的引用放在常量池中String s3 = new String("2") + new String("2");
仅在堆中创建了对象"22","22"字面量没有放入字符串常量池。String a3 = a1+a2;
同3.String s6 = "11" + "22";
在编译阶段就会将这两个字符串常量进行拼接,也就是"1122",并放入字符串常量池中。
public class Main {
public static void main(String[] args) {
String s1 = new String("123");
String s2 = "123";
System.out.println(s1==s2); //false
s1 = s1.intern(); //把堆中s1对象的引用放在常量池中
System.out.println(s1==s2); //true
String s3 = new String("2") + new String("2"); //仅在堆中创建了对象"22","22"字面量没有放入字符串常量池
String s4 = "22"; //"22"字面量放入字符串常量池
System.out.println(s3==s4); //false
s3 = s3.intern(); //把堆中s3对象的引用放在常量池中
System.out.println(s3==s4); //true
String a1 = "11";
String a2 = "22";
String s5 = "1122";
String a3 = a1+a2;
System.out.println(a3==s5); //false
a3 = a3.intern();
System.out.println(a3==s5); //true
String s6 = "11" + "22"; //在编译阶段就会将这两个字符串常量进行拼接,也就是"1122",并放入字符串常量池中。
System.out.println(s5==s6); //true
}
}
- 内存中有一个java基本类型封装类的常量池。这些类包括Byte, Short, Integer, Long, Character, Boolean。需要注意的是,Float和Double这两个类并没有对应的常量池。
- Integer的常量池为: -128~127
- "=="两边有算术运算时才会自动拆箱,因此此时比较的是数值,而并非对象
public class Main {
public static void main(String[] args) {
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1==i2); //true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3==i4); //false
Integer i5 = new Integer(127);
Integer i6 = new Integer(0);
System.out.println(i1==i5); //false
System.out.println(i3==i4+0); //true
System.out.println(i1==i5+0); //true
}
}
7、继承相关
- 子类不可以继承父类的构造方法,只可以调用父类的构造方法。在子类的构造方法中调用父类的构造方法是用super(),如果没有写super(),则默认调用父类的无参构造方法。一个类都会有默认的空参数的构造函数,若指定了带参构造函数,那么默认的空参数的构造函数,就不存在了。这时如果子类的构造函数有默认的super()语句,那么就会出现错误,因为父类中没有空参数的构造函数。