1. Java 语言的特点(一般与 C++ 对比)
- Java 具有内存自动分配与垃圾收集技术(C++ 需要程序员自己分配)
- Java 跨平台,即『一次编写,到处运行』。在引入虚拟机之后,Java 在不同平台上运行不需要重新编译。
- Java 是静态语言,强类型语言。
- Java 既不是编译型语言(如 C++),也不是解释性语言(如Python),而是编译与解释共存。
- 编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码,执行速度比较快,但开发效率比较低。
- 解释型语言会通过解释器一句一句的将代码解释为机器代码后再执行,开发效率比较快,但执行速度比较慢。
- Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码文件,这种字节码必须由 Java 解释器来解释执行(为了提升效率,会配合 JIT 及时编译期执行)。
- Java 不支持类的多继承,而 C++ 支持,但是 Java 允许实现多接口。
2. JDK、JRE、JVM 之间关系
JVM 是整个 Java 实现跨平台的最核心的部分,但是只有 JVM 并不能运行程序;
如果想要运行一个 Java 程序,可以只安装 JRE,不安装 JDK;
如果想要开发一个 Java 程序 ,必须安装 JDK;
JVM(Java Virtual Machine)Java 虚拟机。主要负责将字节码文件解释成具体的平台能够识别的机器指令。JRE(Java Runtime Environment)Java 运行环境。JRE = JVM + Java 程序执行所需要的系统类库。系统类库比如:java.lang 包、java.util 包JDK(Java Development Kit)Java 开发工具包。JDK = JRE + Java 程序所必须的编译、运行等开发工具。开发工具比如:用于编译 Java 程序的 javac 命令、用于启动 JVM 运行 Java 程序的 java命令、用于生成文档的 javadoc 命令
3. 面向对象和面向过程的区别
面向过程
- 一种以过程为中心的编程思想,把问题分解成一个一个步骤,每个步骤用函数实现,然后依次进行调用。
- 优点:很明确每一步在做什么,并且执行效率更高。
- 缺点:代码重用性低,扩展能力差,后期维护难度比较大
面向对象
- 对问题的每个步骤进行抽象,形成对象,通过对象执行方法的方式组合解决问题。
- 优点:开发程序更易维护、易复用、易扩展。
4. 面向对象三大特性是什么【⭐⭐⭐⭐】
封装封装是指把一个对象的状态信息隐藏在对象内部,不允许外部对象直接访问对象的内部信息。通过提供一些可以被外界访问的方法来对属性进行操作。
- 良好的封装能够减少耦合。
- 隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性以及安全性。
- 可以对成员变量进行更精确的控制。(使用者对类内部定义的属性直接操作可能会导致数据的错误、混乱或安全性问题。)
功能:信息隐藏
继承继承是使用已存在的类(父类)作为基础建立新类(子类)的技术,子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。
功能:代码复用
多态同一行为发生在不同的对象上会产生不同的结果。(同一行为,具有多个不同表现形式)
程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
功能:提高了代码的扩展性
使用多态的前提:
- ① 存在继承或者实现关系
- ② 子类覆盖(重写)父类方法
- ③ 向上转型(父类引用指向子类对象)
5. Java 基础数据类型
Java 存在 8 种基本数据类型,共分为四类:
- 整数型(byte、short、int、long)
- 浮点型(float double)
- 字符型(char)
- 布尔型(boolean)
| 数据类型 | 关键字 | 内存占用/字节 | 取值范围 | 最小值符号 | 最大值符号 |
|---|---|---|---|---|---|
| 字节型 | byte | 1 | -128 ~ +127 | Byte.MIN_VALUE | Byte.MAX_VALUE |
| 短整型 | short | 2 | -32768 ~ +32767 | Short.MIN_VALUE | Short.MAX_VALUE |
| 整型 | int | 4 | -2^31 ~ 2^31-1(超过20亿) | Integer.MIN_VALUE | Integer.MAX_VALUE |
| 长整型 | long | 8 | -2^63 ~ 2^63-1 | Long.MIN_VALUE | Long.MAX_VALUE |
| 单精度浮点型 | float | 4 | 1.4E-45 ~ 3.4028235E38(有效位数6~7位) | Float.MIN_VALUE | Float.MAX_VALUE |
| 双精度浮点型 | double | 8 | 4.9E-324~1.7977E+308(有效位数15位) | Double.MIN_VALUE | Double.MAX_VALUE |
| 字符型 | char | 2 | 0 ~ 65535 | Character.MIN_VALUE | Character.MAX_VALUE |
| 布尔型 | Boolean | 1 | true、false | * | * |
注意:Java 没有任何无符号 (unsigned) 形式的 int、long、short 或 byte 类型,但可以通过其包装类转换得到。
6. == 和 equals 的区别
- ==
- 对于基本数据类型,== 比较的是数值是否相等
- 对于引用数据类型,== 比较的是引用(地址)是否相等
- equals
- equals 不能比较基本数据类型是否相等(可以转化为对应的包装类后再进行比较)
- Object 类中的 equals 的实现用的是
this==obj,而所有引用数据类型均继承了 Object 类,因而默认情况下,equals 方法的作用和 == 比较引入数据类型的规则一致,均比较的引用(地址)是否相等。 - 通常情况下,我们想要比较的是数值(属性)是否相等,因而会对 equals 进行重写,满足自身需求,像String、Date、File、包装类等都重写了 Object 类中的 equals() 方法。重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的”实体内容”是否相同。。
- 重写 equals 方法的建议(Java核心技术,卷11)
- 检测 this 与 otherObject 是否相等,if(this == otherObject) return true;
- 检测 otherObject 是否为 null,if(otherObject == null) return false;
- 判断 this 和 otherObject 是否属于相同类 if(getClass() != otherObject.getClass()) return fasle;
- 根据相等性概念的要求比较字段。使用 == 比较基本数据类型,使用 Objects.equals(本字段,所比较字段)比较对象字段。
7. 重写 equals() 时为什么要重写 hashCode() 【⭐⭐⭐⭐】
equals 和 hashCode 都是在 Object 中存在的方法,默认情况下:
equals方法的作用和 == 比较引入数据类型的规则一致,均比较的引用(地址)是否相等。hashCode方法会返回根据对象内存地址做相应转化得到的哈希值。
在 HashSet 和 HashMap 等集合中,为了提升增删改查的效率,在存入数据时,一般首先根据对象的 hashcode 方法计算对象应该存储的位置,然后再根据 equals 方法判断同一个位置上的对象是否相等。
如果我们重写了 equals 方法,比较对象的属性是否相等,此时如果不重写 hashcode 方法,那么可能会出现以下情况,我们 new 了两个属性值完全相等的对象 s1 和 s2
1 | Student s1 = new Student("张三",23); |
此时 s1 和 s2 通过 equal 方法判断是相等的,但是通过 hashcode 方法判断可能是不相等的(因为 s1 和 s2 的地址不相同)。在 set 集合中添加 s1 和 s2 时,我们希望得到的结果是两个对象通过去重得到唯一结果,但可能会出现错误。比如:
- 我们首先在 set 集合 中添加 s1 ,假设通过 hashcode 计算出的索引位置为 1,我们将 s1 存放在 1 号位置。
- 而再向 set 集合 中添加 s2 时,通过 hashcode 计算的索引位置可能是 2 ,我们将 s2 存放在 2 号位置。
- 此时就出现了问题,本来 equals 判断相等的两个对象竟然都被存放到了 set 集合中!这是我们不希望看到的!
因此当我们重写 equals() 方法时,最好也要重写一下 hashCode()。
一般规定:
- equals 相等,hashCode 应该也相等。
- hashCode 相等,equals 不一定相等。
8. Java 方法参数是值传递还是引用传递?
值传递:是指在调用函数时将实际参数复制一份传递到函数中,这样在方法中如果对参数进行修改,将不会影响到实际参数。
引用传递:是指在调用函数时将实际参数的地址直接传递到函数中,那么在方法中对参数所进行的修改,将影响到实际参数。
Java 里方法的参数传递方式只有一种:值传递。(即将实际参数值的副本传入方法内,而参数本身不受影响。)
- 如果形参是基本数据类型,传递的是基本类型的字面量值的拷贝;
- 如果形参是引用数据类型,传递的是该参量所引用的对象在堆中地址值的拷贝。
9. 静态变量、成员变量、局部变量的区别
| 在类中的位置 | 作用范围 | 初始化值 | 修饰符 | 内存位置 | |
|---|---|---|---|---|---|
| 静态变量 | 类中,方法外 | 类中 | 有默认值(数值为 0,布尔值为 false,引用为null) | public、private、final 等 | 方法区 |
| 普通成员变量 | 类中,方法外 | 类中 | 有默认值(数值为 0,布尔值为 false,引用为 null) | public、private、final 等 | 堆区 |
| 局部变量 | 方法中或者方法声明上 (形式参数) | 方法中 | 没有默认值。必须先定义,赋值,最后使用 | 不能用权限修饰符修饰,可以用final修饰 | 栈区 |
10. Java 权限修饰符区别
Java 权限修饰符 public、protected、default(缺省)、private 置于类的成员定义前,用来限定对象对该类成员的访问权限。
- public : 对所有类可见。使用对象:类、接口、变量、方法
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
以下是访问控制级别:
public > protected > (default) > private
| 修饰符 | 同一个类 | 同一个包 | 不同包的子类 | 同一个工程 |
|---|---|---|---|---|
| public | √ | √ | √ | √ |
| protected | √ | √ | √ | |
| default(缺省) | √ | √ | ||
| private | √ |
注意:对于 class 的权限修饰只可以用 public 和 default(缺省)。
11. 重写和重载的区别
重载:同一个类中不同方法具有相同的名字,但是参数不一样,即参数的名称和参数的类型不一样。同类不同参。
重写:子父类的,即子类与父类具有相同的方法名字还有参数参数相同和相同的返回类型。即同名同参同类型。
| 参数列表 | 是否有继承关系 | |
|---|---|---|
| 重载(overload) | 必须不同 | 无继承关系,在同一个类中 |
| 重写(override) | 必须相同 | 有继承关系,在不同类中 |
12. 自动装箱与自动拆箱
为了使基本数据类型的变量具有类的特征,Java 引入了包装类的概念。
自动装箱:将基本数据类型自动转换成对应的包装类。
自动拆箱:将包装类自动转换成对应的基本数据类型。
1 | // 基本数据类型---->包装对象【装箱】 |
特殊:在 Java 1.5 后,为了提升性能, Integer 类使用了缓存机制,在自动装箱时,如果数值在 -128 ~ +127 之间,会使用缓存中的对象。
1 | // 自动装箱,数值在缓存区间内 |
13. 常见集合以及其关系

Collection 接口:单列数据,定义了存取一组对象的方法的集合,它有两个重要的子接口:
- List:元素有序且可重复的集合,主要实现类有 ArrayList 、LinkedList 和 Vector 。
ArrayList:作为 List 接口的主要实现类;线程不安全的,效率高;底层使用 Object[] elementData 存储LinkedList:对于频繁的插入、删除操作,使用此类效率比 ArrayList 高;底层使用双向链表存储。Vector:作为 List 接口的古老实现类;线程安全的,效率低;底层使用 Object[] elementData 存储。
- Set: 元素无序且不可重复的集合,主要实现类有 HashSet 、LinkedHashSet 和 TreeSet 。
HashSet:作为 Set 接口的主要实现类;线程不安全的;可以存储 null 值。LinkedHashSet:作为 HashSet 的子类;遍历其内部数据时,可以按照添加的顺序遍历。TreeSet:可以照添加对象的指定属性,进行排序。
Map 接口:双列数据,保存具有映射关系“key-value”的集合,主要实现类有 HashMap,LinkedHashMap,TreeMap。
HashMap:作为 Map 的主要实现类;线程不安全的,效率高;可以存储 null 的 key 和 valueLinkedHashMap:LinkedHashMap 是 HashMap 的子类。在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序,与 LinkedHashSet 类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致。TreeMap: 对添加的 key-value 对进行排序,实现排序遍历。考虑 key 的自然排序或定制排序,底层使用红黑树。Hashtable:作为古老的实现类;线程安全的,效率低;不能存储 null 的 key 和 value,Hashtable 实现原理、功能和 HashMap 相同。
14. ArrayList 和 LinkedList 的区别【⭐⭐⭐⭐】
- ArrayList
- 底层由数组实现,可动态扩容,需要连续内存
- 有索引,因此查询效率高
- 增加或删除尾部元素效率高,但是增加或删除元素到其他位置,需要将数组批量移动,效率不高。
- LinkedList
- 底层由双链表实现,不需要连续内存
- 没有索引,查询需要遍历链表,效率低
- 增加或删除尾部或头部元素效率高,但是增加或删除元素到其他位置,需要首先遍历链表,找到元素位置,效率较低(虽然找到后插入/删除很快)
15. HashMap 底层 【⭐⭐⭐⭐⭐】
- JDK1.7 数组 + 链表
- JDK1.8 数组 + (链表 | 红黑树)
Put 操作流程
- HashMap 是懒惰创建数组的,首次使用才创建数组
- 计算索引(桶下标),先调用 HashMap 的 hash() 方法,在计算对象的 hashCode()后进行二次哈希。二次 hash() 是为了综合高位数据,让哈希分布更为均匀。
- 如果桶下标还没被占用,创建 Node 占位返回。
- 如果桶下标已经被占用
- 已经是 TreeNode 走红黑树的添加或更新逻辑。
- 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑。
- 返回前检查容量是否超过阈值,一旦超过进行扩容
数组容量为何是 2 的 n 次幂
- 计算索引时效率更高:如果是 2 的 n 次幂 可以使用按位与运算代替取模
- 扩容时重新计算索引效率更高: hashcode 值 & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap。等于0时,hash % oldCap == hash % 2*oldCap
树化规则
链表长度 > 8 && 数组容量 >=64- 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化
- 为什么选择 8 呢?hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小,当链表比较短的时候,维护比红黑树更简单方便,并且TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表,而不是直接就使用红黑树,只有当链表长度大于 8 时才会树化。
退化规则
情况1:在扩容时如果拆分树时,如果拆分的树
树中元素个数 <= 6则会退化链表,否则不会退化。情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表
扩容(加载)因子为何默认是 0.75f
- 在空间占用与查询时间之间取得较好的权衡
- 大于这个值,空间节省了,但链表就会比较长影响性能
- 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
1.7 与 1.8 的区别
链表插入节点时,1.7 是头插法,1.8 是尾插法
1.7 是大于等于阈值且当前要插入的元素没有空位时才扩容(也就是如果当前要加入的元素正好有空位可以放下,那就不必扩容),而 1.8 是大于阈值就扩容
1.8 在扩容计算 Node 索引时,会优化
并发问题
- 扩容死链(1.7 会存在),由头插法引起的。
- 数据错乱(1.7,1.8 都会存在)
16.HashMap、HashTable、以及 ConcurrentHashMap 的区别【⭐⭐⭐⭐⭐】
HashMap- 线程不安全,效率高
- 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;
- 在 1.8 的时候更改底层为『数组 + 链表 + 红黑树』
HashTable- 线程安全(内部的方法基本都经过 synchronized 修饰),古老的实现类,效率低,实现原理、功能和 HashMap 相同,基本被淘汰。
- 不允许有 null 键和 null 值,否则会抛出空指针异常
- 底层为『数组 + 链表』
ConcurrentHashMap- 线程安全
- 使用 synchronized 和 CAS 来操作。
- 在 1.8 的时候更改底层为『数组 + 链表 + 红黑树』
17. this 和 super 的区别
| 关键字 | 访问成员变量 | 调用成员方法 | 调用构造方法 |
|---|---|---|---|
| this | 访问本类中的成员变量,如果本类没有,则从父类中继续查找 | 访问本类中的成员方法,如果本类没有,则从父类中继续查找 | 访问本类中的构造方法 |
| super | 直接访问父类中的成员变量 | 直接访问父类中的成员方法 | 访问父类中的构造方法 |
18. 抽象类与接口的对比
- 相同点
- 均不能被实例化。
- 都可以包含抽象方法。
- 接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
- 不同点
- 抽象类中有构造方法,而接口中没有。
- 抽象类不能多继承,而接口可以。
- 接口中的变量必须有static、final修饰,实际是一个常量,必须赋初值,而抽象类可以任意。
19. 泛型与类型擦除
泛型是 JDK1.5 的一个新特性,就是将类型参数化,其在编译时才确定具体的参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
使用泛型可以提高 Java 程序的类型安全,在编译前就能检测是否存在 ClassCastException 异常,并在代码执行时不需要每次使用时自己进行强制类型转换,同时提升了代码的可读性。
泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,在运行时不存在任何类型相关的信息,也就是说:泛型只存在于编译阶段,而不存在于运行阶段。使用类型擦除的主要目的是确保能和 JDK1.5 之前的代码进行兼容,并且实现简单,几乎不需要更改 JVM代码。
20. String、StringBuffer 和 StringBuilder 的区别【⭐⭐⭐⭐】
String:不可变、线程不安全
StringBuilder:可变、线程不安全
StringBuffer:可变、线程安全
性能:String < StringBuffer < StringBuilder
21. 为什么 String 设置为不可变的
String 为什么不可变?
- 保存字符串的数组被
final修饰且为私有的,并且String类没有提供/暴露修改这个字符串的方法。 String类被final修饰导致其不能被继承,进而避免了子类破坏String不可变。
不可变的好处?
可以缓存 hash 值
因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
字符串常量池的需要
如果一个 String 对象已经被创建过了,那么就会从字符串常量池中取得引用。只有 String 是不可变的,才可能使用字符串常量池,如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,字符串常量池也就不是『常量』池了,就成了变量池。
安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。
线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
22. Java 异常分类
Throwable:Throwable 类是 Java 语言中所有错误或异常的超类。类中常用方法有:
public void printStackTrace():打印异常的详细信息。
包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
public String getMessage() :获取发生异常的原因。
提示给用户的时候,就提示错误原因。
Error:程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。比如栈溢出、堆溢出等错误。
Exception:程序本身可以捕获并且可以处理的异常。可以分为两大类:
- 非受检异常(运行时异常):指 RuntimeException 类及其子类异常,编译器不会检查此类异常,并且不要求处理异常。程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。比如数组索引越界异常,空指针异常等等。
- 受检异常(编译时异常):是 RuntimeException 以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常。
23. Java 常见异常
java.lang.ArrayIndexOutOfBoundsException数组索引越界异常。java.lang.NullPointerException空指针异常java.lang.ClassCastException类型转换异常java.lang.ArithmeticException算术异常java.lang.NumberFormatException数字格式异常
24. throw 和 throws 的区别
throw表示抛出一个异常类的对象,生成异常对象的过程。声明在方法体内。可以抛出多个异常,用来标识该方法可能抛出的异常列表。throws属于异常处理的一种方式,声明在方法的声明处,只能用于抛出一种异常,用来抛出方法或代码块中的异常。
25. 深拷贝和浅拷贝【⭐】
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
通过拷贝构造方法实现浅拷贝:
重写 clone() 方法进行浅拷贝:
Object 类虽然有这个方法,但是这个方法是受保护的(被protected修饰)
使用 clone 方法的类必须实现 Cloneable 接口,否则会抛出异常 CloneNotSupportedException
在要使用 clone 方法的类中实现 Clonable接口,并重写 clone() 方法,通过 super.clone() 调用Object类中的原 clone 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Student implements Cloneable {
//引用类型
private Subject subject;
//基础数据类型
private String name;
private int age;
public Object clone() {
//浅拷贝
try {
// 直接调用父类的clone()方法
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Student implements Cloneable {
//引用类型
private Subject subject;
//基础数据类型
private String name;
private int age;
public Object clone() {
//深拷贝
try {
// 直接调用父类的clone()方法
Student student = (Student) super.clone();
student.subject = (Subject) subject.clone();
return student;
} catch (CloneNotSupportedException e) {
return null;
}
}深拷贝相比于浅拷贝速度较慢并且花销较大。