八股文骚套路之Java基础
⭐ :面试中不常问到,如果面试官问到尽量能答出来,答不出来也没关系。
⭐⭐ :面试中不常问到,但是如果面试官问到的话,答不出来对你的印象会减分。
⭐⭐⭐:面试中会问到,答不出来面试有点悬。面试官会惊讶为什么你这也不会。
⭐⭐⭐⭐:面试高频考点。
⭐⭐⭐⭐⭐:面试超高频考点。四星考点和五星考点是参加十场面试,至少能有五场面试问到这些的。大家在准备面试过程中尽量把这些知识点的回答条理梳理清楚,面试官一问就开背。
🆗Java 语言的特点⭐️⭐️
- 语法简单,上手容易
- 面向对象(封装 继承 多态)
- Java 虚拟机实现了 Java 语言的跨平台性
- 支持多线程
- 可靠(异常处理和自动内存管理机制)、安全(多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源)
- 高效性(预编译等技术)
- 支持网络编程
- 编译与解释并存
🆗比较 JVM 和 JDK 以及 JRE ⭐️⭐️⭐️
- JVM(Java Virtual Machine) Java 虚拟机是运行 Java 字节码的虚拟机
- JDK(Java Development Kit) Java 开发工具包,提供 Java 的开发环境,包含 JRE
- JRE(Java Runtime Environment) Java 运行时环境,包含 JVM 和一些核心类库
总结: JDK 包含了 JRE 和开发工具, JRE 包含了 JVM 和核心类库
🆗为什么说 Java 解释与编译并存 ⭐️⭐️
- 高级编程语言按照程序的执行方式分为编译型和解释型
- 编译型是通过编译器一次性将源码翻译为机器码,而解释型是通过解释器逐句将代码解释为机器码再执行。
- Java 程序执行需要先由编译器编译为字节码 .class 文件,再由 Java 解释器解释执行。
🆗Java 基本类型有哪几种,各占多少位?⭐️⭐️
基本类型有8种
6种数字类型:
4种整型:byte short int long (分别为8 16 32 64)
2种浮点型:float double(分别为32 64)
1种字符型:char(16)
1种布尔型:boolean(8)
🆗Java中的泛型和类型擦除 ⭐️⭐️⭐️
泛型是Java的一个新特性,可以用来增强可读性和稳定性。
Java编译器可以对泛型参数进行检测,可以指定传入对象的泛型
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法
但是泛型是语法糖的一种,Java虚拟机里没有泛型,只有普通类和普通方法,Java虚拟机会在编译阶段通过类型擦除的方式进行解语法糖,类型擦除就是将所有的泛型参数用其最顶级的父类进行替换。
🆗既然编译器要将泛型擦除,那为什么还要使用泛型,用Object代替不行吗?(使用泛型的好处)
- 使用泛型可在编译期前进行类型检测。
- 使用 Object 类型需要手动添加强制类型转换,降低代码可读性,提高出错概率。
🆗== 和 equals() 的区别⭐️⭐️⭐️
这两者对于基本类型和引用类型的效果是不同的
- 对于基本数据类型,== 比较的是值;对于引用数据类型,==比较的是内存地址
因为在Java中只有值传递,所以本质上来说 == 比较的就是值。
- equals()不能用来判断基本数据类的变量,只能用来判断两个对象是否相等,这有两种情况:第一种是没有重写equals方法,那么默认比较的是两个对象的地址,等同于==;但是一般会重写equals方法来判断两个对象的属性是否相等,若属性相等则返回true认为两个对象相等
🆗hashCode() 和 equals() ⭐⭐⭐⭐
hashCode是用来获取散列码,用来确定对象在hash表中的索引位置,帮助快速找到所需要的对象。
当把对象加入hashset,hashset会先计算当前对象的哈希值来确定该对象加入的位置,同时也会将这个哈希值和已经加入对象的哈希值作比较,如果没有一样的,hashset会假设这个对象没有重复出现。但是如果发现有相同的哈希值的对象,则更进一步用equals方法来检查两个对象是不是真的相同。如果相同那么就不会让这个对象加入;如果不同,那么会重新将这个对象散列到其他位置。这样可以大大减少equals方法的执行速度。
- 重写equals的时候必须重写hashcode()方法,因为hashcode相等是两个对象相等的必要不充分条件。
🆗重载和重写的区别⭐⭐⭐⭐
- 重载是同一个类中多个同名方法,根据输入参数的数据类型或者数量的不同,作出不同的处理
- 重写是子类继承父类的相同方法时,输入数据是一样的,但是需要作出不同于父类的处理,比如功能的扩展之类的,需要覆盖父类的方法(重写发生在运行时,可以改变内部逻辑,但是外部的样子不能改变)
🆗深拷贝和浅拷贝
- 深拷贝会完全复制整个对象,包括这个对象的内部对象。
- 浅拷贝会在堆上创建一个新的对象,但是如果对象内部是引用类型的话,不会拷贝内部对象,而是拷贝内部对象的地址引用
- 引用拷贝则是只拷贝引用地址
成员变量和局部变量的区别⭐️⭐️⭐️
- 成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被访问控制修饰符修饰,局部变量不能;但是两者都可以被final修饰。
- 生命周期上,成员变量是对象的一部分,生命周期和对象一致,而局部变量是方法的,随着方法的调用和调用结束产生和消亡
- 默认值:成员变量如果没被赋值那么会有默认值(final修饰的成员变量必须显示赋值);而局部变量不会被自动赋值。
面向对象三大特性是什么。并解释这三大特性。⭐️⭐️⭐️⭐️
- 三大特性是封装、继承和多态
封装是把一个对象的属性隐藏在对象内部,不允许外部对象直接访问对象的内部信息
- 继承
- 子类继承父类对象所有的属性和方法,但是父类中的私有属性和方法子类只是拥有不能访问。
- 子类可以对父类的属性和方法进行扩展(子承父业,发扬广大)
- 子类可以重写(overwrite)父类的方法, 在其基础之上实现功能的扩展
- 多态
一个对象拥有多种状态,父类的引用指向子类的实现
- 对象类型和引用类型之间存在继承/实现接口的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须到程序运行时才能确定;3. 多态不能调用“只在子类中有,但是父类中没有的”的方法;
- 如果子类重写了父类的方法,真正执行的是子类中重写的方法,如果子类没有重写父类的方法,执行的是父类的方法。(就近原则)
String、StringBuffer 和 StringBuilder 的区别。 ⭐⭐⭐⭐
从可变性上来说,String是不可变的。而StringBuffer和StringBuilder是可变的
从线程安全性上来说,String中的对象是不变的,所以线程安全(可理解为常量
而StringBuffer添加了同步锁,线程安全
而StringBuilder没有加锁,所以线程不安全
从性能上讲:
Java异常⭐️⭐️⭐️
Java中,所有的异常都有一个共同的祖先Throwable类,这个类主要有两个重要的子类
Exception 和 Error
- Exception 是程序本身可以处理的异常,可以通过catch来捕获。
- Exception又可以分为 Checked Exception可检异常(必须要处理)和Unchecked Exception不可检异常(可以不处理)
- 可检异常有:ClassNotFoundException(加载不存在的类时抛出的异常)、SQLException(与数据库交互时候的抛出的异常)、IOException(输入输出操作时候的异常)、ParseException(对日期和时间解析时可能抛出的异常)、FileNoteFoundException(尝试打开一个存在的文件时抛出的异常)
- 不可检异常:
- NullPointerException(空指针错误)
- IllegalArgumentException(参数错误比如方法入参类型错误)
- NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
- ArrayIndexOutOfBoundsException(数组越界错误)
- ClassCastException(类型转换错误)
- ArithmeticException(算术错误)
- SecurityException (安全错误比如权限不够)
- UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
- Error: Error属于程序无法处理的错误,没法通过catch来捕获。异常发生时Java虚拟机一般会选择终止。
TODO: 异常捕获的代码
1 | try{ |
🆗序列化和反序列化 ⭐️⭐️
序列化:将数据结构对象转化为二进制字节流的过程
反序列化:将在序列化过程中生成的二进制字节流转化成数据结构或者对象的过程
序列化的主要目的是通过网络对象或者是将对象存储到文件系统、数据库、内存中。
反射 ⭐⭐ 面试官可能会问你什么是反射,它的优缺点是什么,有哪些应用场景
通过反射可以获取任意一个类的所有属性和方法,并且可以调用这些方法和属性
优点:
- 反射可以让代码更加灵活、为各种框架开发提供了便利
缺点:
- 安全问题,可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)
- 性能稍微差点
反射的应用场景:
- 框架中大量使用了动态代理,而动态代理的实现依赖反射
- 注解的实现也用到了反射
List、Set、Map、Queue的区别 ⭐⭐
- List: 存储的元素是有序的、可重复的。
- Set: 存储的元素不可重复的。
- Queue: 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
- Map: 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),”x” 代表 key,”y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值
ArrayList 和 LinkedList 的区别。⭐⭐⭐⭐
线程安全性:ArrayList 和 LinkedList 都是线程不安全的。
底层数据结构:ArrayList底层采用object数组存储,LinkedList底层采用的是双向链表
插入和删除元素:
- ArrayList采用数组存储,所以插入和删除元素的复杂度受到元素位置的影响
- 而LinkedList 采用双向链表 所以再头尾插入元素不受元素位置的影响。而在其他位置插入元素的话需要先遍历到那个位置。
随机访问:ArrayList支持随机访问,而LinkedList不支持随机访问。
内存占用:ArrayList占用连续的内存空间,且在list列表的表尾会预留一部分空间导致内存的浪费;而LinkedList中的每一个元素都会占据比ArrayList更多的空间,因为要存放前驱+后继+元素
我们在项目中一般是不会使用到 LinkedList 的,需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且,性能通常会更好!
比较HashSet、LinkedHashSet 和 TreeSet 三者的异同。【⭐⭐⭐】
- HashSet、LinkedHashSet 和 TreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
- HashSet、LinkedHashSet 和 TreeSet 的主要区别在于底层数据结构不同。
HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。
LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。
TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。 - 底层数据结构不同又导致这三者的应用场景不同。
HashSet 用于不需要保证元素插入和取出顺序的场景
LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景
TreeSet 用于支持对元素自定义排序规则的场景
HashMap 多线程操作导致死循环问题。【⭐⭐⭐】
Java1.7之前 HashMap在多线程的环境下扩容操作可能存在死循环的问题,因为当有多个元素需要扩容的时候,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,形成环形链表。Java1.8之后改为尾插法解决了这个问题。
但是多线程使用HashMap还容易导致数据覆盖的问题,并发环境下可以使用ConcurrentHashMap
HashMap 的长度为什么是 2 的幂次方。【⭐⭐⭐】
为了尽量减少hash冲突,hash值的范围很大,但是这么大的映射空间的数组内存是放不下的,所以用之前会对数组进行取模运算,得到的余数才是对应的最终的数组下标,所以这样操作主要是考虑到了对运算效率的提升。
在设计算法的时候取余操作只有当除数是 2 的幂次才等价于与其除数减一的与(&)操作
(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)
HashMap、HashTable、以及 ConcurrentHashMap 的区别⭐⭐⭐⭐⭐
- 首先HashMap和HashTable
从线程安全性上来说:
HashMap是非线程安全的,HashTable是线程安全的,因为Hash的大部分方法都使用了synchronized修饰(但是如果想保证线程安全 还是要用ConcurrentHashMap)
从效率上来说:
因为线程安全问题,hashMap比HashTable效率高,而且hashtable有上位替代ConcurrentHashMap
从null key 和 null value 的支持上来说:HashMap可以存储null的key和value,但是null作为key只能有一个,作为值可以有多个;HashTable不允许有NUll键和Null值,否则会抛NullPointerException
从初始容量大小和每次扩充的的容量大小上来说:
- 如果不指定容量的初始,Hashtable默认为11 ,之后每次扩充扩充为原来容量的2N+1;HashMap默认大小为16,之后每次扩充为原来两倍。
- 如果指定容量的初始值,hashtable会直接用你指定的大小,而hashmap会将其扩充为2的幂次方大小。
从底层数据结构上来说:
Java1.8之后的HashMap在解决哈希冲突时有了很大变化,当链表长度大于阈值的时候,将链表转化为红黑树以减少搜索的时间(在转化成红黑树之前会先判断当前数组的长度是否小于64,如果小于则先进行扩容,而不是转化成红黑树),而HashTable没有这样的机制。
- ConcurrentHashMap 和 Hashtable 的区别
两者主要的区别是实现线程安全的方式不同
从底层数据结构上来说: JDK1.7 的ConcurrentHashMap采用分段数组+链表
实现,JDK1.8采用的数据结构和HashMap1.8一样,采用数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似,都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
从实现线程安全的方式上来说:并发HashMap有多把锁,而Hashtable共用一把锁
在 JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割(segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
到了 JDK1.8 的时候,ConcurrentHashMap不用segment了,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作
Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,效率十分低下
什么是 CAS? CAS 即比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg 。
JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?
- 线程安全实现方式:JDK 1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
- Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
- 并发度:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
为什么要用红黑树
主要目的是解决哈希冲突
当哈希冲突发生时,通常会使用链表将具有相同哈希值的键值对存储在同一个桶中。
然而,当链表中的元素达到一定的阈值的时候,Java1.8会将链表转化为红黑树,红黑树是一种平衡二叉搜索树,具有更快的查找效率,可以解决链表过长的时候查找效率低下的问题
static 和 final 一起使用的效果
需要看修饰的是变量还使类
- static final 变量:static代表变量属于类,final代表不可变,因此加在一起的话这个变量就是常量
- static final 方法:static代表该方法属于当前类,不属于实例,不能被重写
- final 类:不能被继承。
- static 类:只能用于内部类,表示该类可以独立于外部类的实例存在。