大家好,我是 moon。
「《面试八股文》之 Java 基础 34卷」 它来了,本文总共针对基础给了 34 个问题,又是小小 1W 字,理解它,看透它~
java 是一门「开源的跨平台的面向对象的」计算机语言.
跨平台是因为 java 的 class 文件是运行在虚拟机上的,其实跨平台的,而「虚拟机是不同平台有不同版本」,所以说 java 是跨平台的.
面向对象有几个特点:
「优点」:
1.良好的封装能够「减少耦合」,符合程序设计追求'高内聚,低耦合' 2.「类内部的结构可以自由修改」 3.可以对成员变量进行更「精确的控制」 4.「隐藏信息」实现细节
- 2.「继承」
- 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
「优点」:
1.提高类代码的「复用性」 2.提高了代码的「维护性」
- 3.「多态」
- 1.「方法重载」:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同。
- 2.「对象多态」:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。
- 多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态:
「优点」
- 「消除类型之间的耦合关系」
- 「可替换性」
- 「可扩充性」
- 「接口性」
- 「灵活性」
- 「简化性」
java 主要有两种数据类型
重写:
重载:
Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
Throwable又派生出「Error类和Exception类」。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
处理方法:
try{
// 程序代码
}catch(ExceptionName e1){
//Catch 块
}
只记录「重点」
不同点 | hashMap 1.7 | hashMap 1.8 |
---|---|---|
数据结构 | 数组+链表 | 数组+链表+红黑树 |
插入数据的方式 | 头插法 | 尾插法 |
hash 值计算方式 | 9次扰动处理(4次位运算+5次异或) | 2次扰动处理(1次位运算+1次异或) |
扩容策略 | 插入前扩容 | 插入后扩容 |
在 「hashMap1.7 中扩容」的时候,因为采用的是头插法,所以会可能会有循环链表产生,导致数据有问题,在 1.8 版本已修复,改为了尾插法
在任意版本的 hashMap 中,如果在「插入数据时多个线程命中了同一个槽」,可能会有数据覆盖的情况发生,导致线程不安全。
只记录「重点」
不同点 | concurrentHashMap 1.7 | concurrentHashMap 1.8 |
---|---|---|
锁粒度 | 基于segment | 基于entry节点 |
锁 | reentrantLock | synchronized |
底层结构 | Segment + HashEntry + Unsafe | Synchronized + CAS + Node + Unsafe |
上图是 set 家族整体的结构,
set 继承于 Collection 接口,是一个「不允许出现重复元素,并且无序的集合」.
HashSet 是「基于 HashMap 实现」的,底层「采用 HashMap 来保存元素」
元素的哈希值是通过元素的 hashcode 方法 来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
泛型:「把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型」
因为泛型其实只是在编译器中实现的而虚拟机并不认识泛型类项,所以要在虚拟机中将泛型类型进行擦除。也就是说,「在编译阶段使用泛型,运行阶段取消泛型,即擦除」。擦除是将泛型类型以其父类代替,如String 变成了Object等。其实在使用的时候还是进行带强制类型的转化,只不过这是比较安全的转换,因为在编译阶段已经确保了数据的一致性。
「进程是系统资源分配和调度的基本单位」,它能并发执行较高系统资源的利用率.
「线程」是「比进程更小」的能独立运行的基本单位,创建、销毁、切换成本要小于进程,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。
「Java 中有 8 个基本类型,分别对应的 8 个包装类」
「为什么需要包装类」:
Integer a = 1000,Integer b = 1000,a==b 结果为「false」
Integer a = 1,Integer b = 1,a==b 结果为「true」
这道题主要考察 Integer 包装类缓存的范围,「在-128~127之间会缓存起来」,比较的是直接缓存的数据,在此之外比较的是对象
JMM 就是 「Java内存模型」(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)「屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果」。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。「线程不能直接读写主内存中的变量」。
每个线程的工作内存都是独立的,「线程操作数据只能在工作内存中进行,然后刷回到主存」。这是 Java 内存模型定义的线程基本工作方式。
有「五种创建对象的方式」
Person p1 = new Person();
Person p1 = Person.class.newInstance();
Constructor<Person> constructor = Person.class.getConstructor();
Person p1 = constructor.newInstance();
Person p1 = new Person();
Person p2 = p1.clone();
Person p1 = new Person();
byte[] bytes = SerializationUtils.serialize(p1);
Person p2 = (Person)SerializationUtils.deserialize(bytes);
直接贴代码
// 懒汉式
public class Singleton {
// 延迟加载保证多线程安全
Private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
Lock lock = new ReentrantLock();
lock. lock();
try {
System. out. println("获得锁");
} catch (Exception e) {
} finally {
System. out. println("释放锁");
lock. unlock();
}
在 Java1.6 之前的版本中,synchronized 属于重量级锁,效率低下,「锁是」 cpu 一个「总量级的资源」,每次获取锁都要和 cpu 申请,非常消耗性能。
在 「jdk1.6 之后」 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了,Jdk1.6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,「增加了锁升级的过程」,由无锁->偏向锁->自旋锁->重量级锁
增加锁升级的过程主要是「减少用户态到核心态的切换,提高锁的效率,从 jvm 层面优化锁」
cas 叫做 CompareAndSwap,「比较并交换」,很多地方使用到了它,比如锁升级中自旋锁就有用到,主要是「通过处理器的指令来保证操作的原子性」,它主要包含三个变量:
当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给 A,基于 A 进行计算,得到新值 B,在用预期原值 A 和内存中的共享变量值进行比较,「如果相同就认为其他线程没有进行修改」,而将新值写入内存
「CAS的缺点」
ReentrantLock 意为「可重入锁」,说起 ReentrantLock 就不得不说 AQS ,因为其底层就是「使用 AQS 去实现」的。
ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。
「公平锁」
「非公平锁」
public class Demo extends Thread{
//重写父类Thread的run()
public void run() {
}
public static void main(String[] args) {
Demo d1 = new Demo();
Demo d2 = new Demo();
d1.start();
d2.start();
}
}
public class Demo2 implements Runnable{
//重写Runnable接口的run()
public void run() {
}
public static void main(String[] args) {
Thread t1 = new Thread(new Demo2());
Thread t2 = new Thread(new Demo2());
t1.start();
t2.start();
}
}
public class Demo implements Callable<String>{
public String call() throws Exception {
System.out.println("正在执行新建线程任务");
Thread.sleep(2000);
return "结果";
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
Demo d = new Demo();
FutureTask<String> task = new FutureTask<>(d);
Thread t = new Thread(task);
t.start();
//获取任务执行后返回的结果
String result = task.get();
}
}
public class Demo {
public static void main(String[] args) {
Executor threadPool = Executors.newFixedThreadPool(5);
for(int i = 0 ;i < 10 ; i++) {
threadPool.execute(new Runnable() {
public void run() {
//todo
}
});
}
}
}
Object obj = new Object();
//只要obj还指向Object对象,Object对象就不会被回收
垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理。
对象内存布局