1.1 简介
此文主要内容:
- Java面向对象三大特征
- ArrayList与LinkedList的区别
- 集合中哪些是线程安全,哪些不是线程安全的
- 如果对于员工入职时间进行排序,应该使用什么集合
- HashMap的底层原理
- 线程生命周期
- JVM内存模型
- 常量池
- 类加载机制
- GC机制
1.2 内容
1.2.1 三大特征
- 封装: 从狭义上来讲,封装即是属性私有化,并提供共有的数据访问函数,提高了程序的安全性。从广义上来讲,封装是集成与包装,例如:Mybatis集成JDBC、SpringBoot 集成 SpringMVC、SpringCloud集成SpringCloud、zlt-mp集成SpringCloud Alibaba。
- 继承: 在父类基础之上,创建子类,子类可以集成父类大部分的属性与方法,提高了代码的复用性,例如:equas是Object超类的方法,Java中的任何一个类都继承了此方法。
- 多态: 对于同一个行为,具有不同的表现形式或形态,提高了代码的可扩展性。例如,对于同一个接口,不同的实例调用同一个方法,会表现出不同的结果。
注意: equals在Object中,就是 使用“==” 进行判断 的,而在String重写后,则是 先使用 “==”进行地址判断 ,然后使用instanceof进行 String类型判断 ,最后使用char[]进行挨个 字符判断 。
1.2.2 ArrayList与LinkedList的区别
- ArrayList的Array即是数组,所以ArrayList的底层是数组;
- LinkedList的Linked即是链,所以LinkedList的底层是双向链表;
- 由于底层是数组,所以ArrayList查询快(索引取值),增删慢(因为需要移动元素);
- 由于底层是链表,LinkedList查询慢(遍历取值),增删快(改变节点引用)。
注意: ArrayList默认长度为10,负载因子为0.75。
1.2.3 集合中哪些是线程安全,哪些不是线程安全的
- 单列集合中,Vector是线程安全的,而ArrayList与LinkedList都不是;
- 双列集合中,HashTable是线程安全的,HashMap则不是。此外,ConcurrentHashMap内部则是使用了乐观锁(CAS)机制,减少了线程安全问题,但是并不能完全保证线程安全。
数据库加乐观锁,是否能够完全做到线程安全?
1.2.4 如果对于员工入职时间进行排序,应该使用什么集合
答案: 使用TreeSet实现,根据其提供的Comparable或者Comparator来进行排序。
Set集合的特点是无序且不可重复,但值得一提的是,无序指的是,存储顺序与插入顺序不同,但元素存储有指定规则的。例如HashSet是根据hash值进行的元素存储位置的计算,而TreeSet的可以指定,存储顺序规则,默认自然排序。
1.2.5 HashMap的底层实现原理
HashMap是双列集合,即键值对,其底层是数组+链表+红黑树。
首先,当元素存入HashMap中,内部根据hash方法与key计算出元素在数组中的存储位置,数组默认长度为16。
其次,由于不同的数据,也会计算出相同的哈希值,因此一个位置不能只放一个元素。所以最后决定,在位置中放入链表(Noded对象),当出现hash冲突时,则将其放入对应位置的链表中。值得一提的是,它并不会出作为尾节点出现,而是作为头节点出现,因为大佬们认为 后存多取 。
最后,由于链表的查询十分缓慢,当数据较多时,不利于查询。因此,当某节点下元素个数达到8时,Node对象则会变成TreeNode,也就是链表变成红黑树。而进行删除之后,个数只有6时,则会由红黑树变成链表。
负载因子也是0.75,而默认长度则是16。负载因子过小容易造成空间浪费,负载因子过大,容易加入Hash冲突。
1.2.6 线程生命周期
1.2.6.1生命周期
- 当new Thread执行时,则线程处于创建状态;
- 当执行start()时,线程则处于就绪状态,并没有执行;
- 当CPU给该线程分配时间片之后,则线程处于运行状态;
- 当线程执行,等待锁,或者调wait进行等待、或者调用sleep进行睡眠之后,线程则处于阻塞状态;
- 当获取到锁之后、或者被notify唤醒、或者睡眠时间已过之后,线程则转为就绪状态;
- 当线程处于运行状态,而未在时间片中执行完成之后,则会转换成为就绪状态;而线程在时间片中执行完成,则会转成死亡状态。
1.2.6.2 创建线程的方式
- 继承Thread,实现run方法;
public class MyThread extends Thread{
@Override
public void run(){
System.out.println("方式一");
}
}
//调用
//MyThread th = new MyThread();
//th.start();
- 实现Runnable接口
public class MyThread implements Runnable{
@Override
public void run(){
System.out.println("方式二");
}
}
//调用
//Thread th = new Thread(new MyThread());
//th.start();
- 实现Callable接口
class MyThread<T> implements Callable {
@Override
public T call() throws Exception {
return null;
}
}
//调用
// MyThread<Integer> th = new MyThread();
// FutureTask<Integer> task = new FutureTask<>(th);
// Thread thread = new Thread(task);
// thread.start();
// Integer integer = task.get();
// System.out.println(integer);
线程池创建线程的方法,归根结底应该还是三者之一。每一次线程时间片结束,而线程未完成时,下一次之所以又可以接着执行,是因为JVM中有一个程序计数器,它负责记录程序的执行位置,每一个线程都有一个私有的计数器。
1.2.7 JVM内存模型
- 虚拟机栈: 每一次方法调用,都是一次入栈操作,会在虚拟机栈中创建一个栈帧。栈帧的内容主要是:局部变量表、操作栈(数据操作指令)、动态链接、方法返回地址等。由于栈的特点是先进后出,所以后调用的方法的栈帧,会在先调用的方法的栈帧的上面。而当方法调用完成,栈帧则会删除;加粗样式
- 本地方法栈: 与虚拟机栈类似,只是存储的内容native修饰的方法的栈帧,也就是其他语言实现的方法,尤其是C与C++;
- 堆: 程序运行过程中,所有对象的存储位置;
- 元空间: JDK1.8以前,叫做方法区,只是那时放在堆中,并且只是一个逻辑概念。而1.8开始,元空间便不属于JVM内存中,独立于JVM。主要存放的内容是方法信息、静态变量、常量、常量池等。
虚拟机栈与本地方法站都是私有的,而堆与元空间都是共享的。对于JVM内存的理解,具体可以参考这篇文章,14年的大佬写得很棒。
1.2.8 常量池
- 整数
Integer i1 = 120;
Integer i2 = 120;
System.out.println(i1==2); //true
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i3==i4); //false
注意: 以上代码之所以会有两种不同的结果,是因为Java在内存中,存入了整数常量,而存储的范围则是[-128-127]。所以,i1与i2都是指向的同一个常量,而i3与i4却不是。
- 字符串
String s1 = "张三";
Stirng s2 = "张三";
String s3 = new String("张三");
String s4 = "张三123";
String s5 = "123";
String s6 = "张三"+"123";
String s7 = s1 + s5;
System.out.println(s1==s2); //true
System.out.println(s1==s3); //false
System.out.println(s4==s6); //true
System.out.println(s4==s7); //false
注意: 首先,当一个字符串没有使用new的方式创建时,会去常量池中看,是否存在该字符串。如果存在则直接引用,如果不如在,则存入常量池,在引用。其次,如果使用new创建,那么会在堆中开辟一片空间,变量指向的是堆地址。最后,字符串的 “+” 操作,如果双方都是 " " 的形式,那么Java字节会把两个字符串合并。如果双方有一个是对象,那么底层实现时,会创建一个 StringBuilder来append其他字符串 。
这里的常量池介绍完全不足,需要找寻专业文章,查看常量池详情。
1.2.9 类加载机制
1.2.9.1 Java有四种类加载机制:
- 启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME>\lib目录中,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。(注:仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)
- 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的,或被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):负责加载用户路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,一般情况下该类加载是程序中默认的类加载器。
- 用户自定义加载类
1.2.9.2 双亲委派模型
双亲委派机制指的是,当一个类加载器收到加载请求时,并不会马上区加载,而是会将请求委派给父级。依次递推,直至启动类加载器,如果父级无法执行,那么会逐级下放。主要解决了Java基础类统一加载的问题。
由于请求是逐级下方,所以BootStrap ClassLoader无法使用应用程序加载类,于是在特定情况下,便存在缺陷,例如厂家自定义组件,SPI还需要详细了解。
1.2.10 GC机制
1.2.10.1 简介
GC即是Java的垃圾回收机制,也就是回收内存。而内存的几大分区中,虚拟机栈、本地方法站、PC寄存器(程序计数器)都是私有的,一旦线程结束便会被回收,所以,GC主要回收的是堆。
1.2.10.2 回收机制
- 引用计数法: Java对象中,会存储一个数用来记录引用数,当被一个对象引用那么+1,如果引用没有了则-1。例如Dog d = new Dog,则引用+1;如果d = null ,那么此时引用-1。当引用记录为0时,GC便会判定为垃圾,进行回收。如果两个对象互相引用(A有个属性是B,B有个属性是A),那么计数器则无法判断。
- 可达性分析法: Java使用一套算法,计算出程序中最活跃的对象。然后遍历该对象下的所有一级或多级引用,如果不在该引用关系中的则被判定为垃圾。