Java基础
1、JVM vs JRE vs JDK
思路:可以从他们之间的关系回答,从小到大进行介绍它们之间的关系和不同,比如 jvm < jre < jdk
答案:
- JVM 是运行 Java 字节码的虚拟机。JVM 会根据不同的系统进行特定的设计(Windows、Linux、MacOS),目的是在不同的操作系统上使用相同的字节码可以得到相同的结果,从而实现 “一次编译,处处运行”
- JRE 是 Java 运行时环境,它是运行已经编译好的 Java 程序所需要的内容的集合,除了包含 JVM 以外,主要还包括了 Java 的基础类库(class library)
- JVM 是功能齐全的 Java SDK,是提供给开发者使用的,能够创建和编译 Java 代码,它除了包含 JRE 之外,还包含编译 Java 代码的编译器(javac)以及其他的一些工具比如 jconsole、jmap、javap 等
2、Java 和 C++ 的区别
答案:
虽然,Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
3、标识符和关键字的区别
答案:
- 标识符就是名字,比如类的名称、变量的名称、方法的名称统称标识符
- 关键字就是被赋予了特殊含义的标识符,是已经被 Java 语言赋予了特殊的含义的标识符
4、基本类型和包装类型的区别
思路:从用途、存储方式、占用空间、默认值以及比较方式进行回答
答案:
- 用途:除了定义一些常量和局部变量,我们通常在方法参数或者对象属性上很少直接使用基本数据雷晓宁。包装类可以用在泛型上,但是基本类型不可用
- 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈的局部变量表中,基本数据类型的成员变量存储在堆区。而包装类型属于对象类型,存储在堆区
- 占用空间:包装类型的占用空间要大于基本数据类型
- 默认值:基本数据类型有默认值且不为 null。包装类型不赋值的情况下默认为 null
- 比较方式:基本数据类型可以直接通过 == 进行比较,他们之间比较的是值的大小。而包装类型如果使用 == 进行比较比较的是地址,如果要比较值需要使用 equals() 方法
tips:
- 虚拟机栈通常包括:局部变量表、操作数栈、动态链接、返回地址以及其他的一些附加信息
- 并不是所有对象类型都存储在堆空间,如果经过逃逸分析之后,JIT 会对那些没有逃逸出方法外部的对象可能进行栈上分配的操作
5、了解包装类型的缓存机制吗
答案:
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
6、自动装箱和自动拆箱是什么,原理是什么
答案:
自动装箱:将基本类型用他们的引用类型包装起来
自动拆箱:将包装类型转换为基本类型
装箱的原理就是调用了包装类的 valueOf() 方法,拆箱的原理就是调用了包装类的 xxxValue() 方法,因此频繁拆装箱是比较影响系统的性能的
7、成员变量与局部变量的区别
思路:从语法形式、存储方式、生存时间、默认值角度去看
答案:
- 语法形式:成员变量是属于类的,局部变量是在代码块或者方法中定义的变量或者是方法的参数。成员变量可以被访问修饰符以及 static 修饰,局部变量不能。他们都可以被 final 修饰
- 存储方式:成员变量是属于堆内存的,局部变量存在于栈内存
- 生存时间:成员变量是对象的一部分,它随着对象的创建而存在,对象的销毁而消亡。局部变量是随着方法的调用而生成,方法的结束而消亡
- 默认值:成员变量如果没有被赋初始值,则自动以类型的默认值而复制(除了 final 的成员变量必须显式地赋初始值),局部变量不会自动复制
8、为什么成员变量要有默认值
答案:
- 首先,成员变量在运行期间是可以通过反射等手段动态赋值的,而局部变量是不行的
- 因此对于 javac 编译器来说,局部变量如果没有赋值是很好判断的,可以直接报错,但是由于成员变量是可以运行时动态赋值的,因此就无法判断,如果在没有赋值的情况下动态读取该变量就会出现以外,因此采用自动赋默认值
9、静态方法为什么不能调用非静态成员
思路:从 JVM 加载类的过程的角度去思考
答案:
- 静态方法是属于类的,在类加载的过程中就会分配内存,而非静态方法是属于实例对象的,只有实例对象存在后才能通过实例对象去访问非静态方法
- 由于 JVM 是先加载了静态的方法和成员,因此如果使用了静态的方法去访问非静态成员,此时非静态成员还没有存在于内存中,属于非法的操作
10、重载和重写的区别
答案:
重载:重载是发生在同一个类中(或者父类和子类之间),方法名称相同,方法的参数列表不同(方法的返回值和修饰符不做考虑),发生在编译期间
重写:重写是发生在有继承关系的父子类之间的,当子类继承了父类后,子类对父类允许访问的方法进行重新编写,方法的名称和方法的参数列表完全相同,发生在运行期间,重写有以下规则需要遵守(两同两小一大):
- 两同:方法名相同,形参列表相同
- 两小:子类方法的返回值和抛出的异常要小于等于父类方法的返回值和抛出的异常
- 一大:子类方法的访问权限要大于等于父类方法的访问权限
11、面向对象和面向过程的区别
答案:
两者的主要区别在于解决问题的方式不同:
- 面向过程会把解决问题的过程抽象成一个个的方法,通过一个个的方法去解决问题
- 面向对象会先抽象出对象,通过对象调用方法的方式去解决问题
面向对象开发的程序通常更容易维护、更容易复用、更容易扩展
12、面向对象的三大特征
答案:
封装:封装是指把一个对象的状态信息隐藏在对象的内部,不允许外部对象直接访问对象的内部信息,但是可以提供一些方法供外界访问
继承:继承是使用已有的类的定义作为基础建立新的类的方式,新类的定义可以添加新的属性和方法,提高代码的复用性和可维护性
多态:基于继承而衍生出的特性,表示一个对象具有多个状态,具体表现为父类引用指向子类的实例对象
13、接口和抽象类的共同点和区别
答案:
共同点:
- 都不能被实例化
- 都可以包含抽象方法
- 都可以由默认实现的方法
区别:
- 接口主要用于定义一种规范,对类的行为进行约束,类实现了某个方法就代表该类具有相应的行为。抽象类主要用于代码的复用,强调的是所属的关系
- 一个类只能继承一个类,但能实现多个接口
- 接口中的成员变量只能是静态常量,抽象类中可以有不同访问权限的成员变量。接口中的方法除了默认方法外都是 public 权限的,抽象类中可以有各种权限的方法
14、深拷贝、浅拷贝、引用拷贝
答案:
深拷贝:深拷贝会完整的复制整个对象,包括该对象的内部对象
浅拷贝:浅拷贝会在堆中创建一个新的对象,不过原对象的内部的对象如果是引用类型的话,浅拷贝只会复制该内部对象的引用地址,也就是拷贝对象和元对象是共用一个内部对象的
引用拷贝:不会在堆中创建一个新的对象,只是两个不同的引用指向了同一个对象
15、hashCode() 有什么用
答案:
hashCode 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定对象在哈希表中的索引位置
16、为什么要有 hashCode
答案:
当你把对象加入 HashSet
时,HashSet
会先计算对象的 hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode
值作比较,如果没有相符的 hashCode
,HashSet
会假设对象没有重复出现。但是如果发现有相同 hashCode
值的对象,这时会调用 equals()
方法来检查 hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals
的次数,相应就大大提高了执行速度。
17、为什么重写 equals() 时必须重写 hashCode() 方法
答案:
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
如果没有重写 hashCode() 方法,可能导致在使用 HashMap 或者 HashSet 的时候,会出现两个相等的对象的 hashCode 却不相同,从而插入了相同的对象
18、介绍一下 Java 的异常体系
思路:按照下图继续回答即可
答案:
Throwable 有两大子类,一个是 Error,另一个是 Exception
- Exception 是程序本身可以处理的异常,可以通过 catch 捕获,Exception又分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
- Error 属于程序无法处理的错误,不建议通过 catch 进行捕获,这些异常发生的时候通常会直接终止虚拟机
19、finally 中的代码一定会执行吗
答案:
不一定,如果 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行
另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:
- 程序所在的线程死亡
- 关闭 CPU
20、什么是泛型,泛型有什么作用
答案:
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList
对象只能传入 Person
对象,如果传入其他类型的对象就会报错
21、什么是序列化,什么是反序列化
答案:
序列化:将数据结构或对象转换成二进制流的过程
反序列化:将序列化过程中生成的二进制流转换成数据结构或对象的过程
使用场景如下:网络传输、对象存储到文件、对象存储到数据库、对象存储到内存
22、序列化协议对应于 TCP/IP 4 层模型的哪一层
思路:
回答:
从上图中可以看出,表示层是负责数据吹里的,因此是表示层也就是应用层
23、为什么要使用堆外内存
回答:
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
- 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
24、谈谈 Unsafe 类
思路:从 Unsafe 功能角度入手
- 内存操作
- 内存屏障
- 对象操作
- 数组操作
- CAS 操作
- 线程调度
- Class 操作
- 系统信息
回答:
参考下面这个链接
https://javaguide.cn/java/basis/unsafe.html
25、Java 中常见的语法糖
回答:
Java 虚拟机本身并不支持这些语法糖。因此这些语法糖都是会在编译阶段被还原为简单的基础语法结构,这个过程就是语法糖
- switch 支持 String 与 枚举(通过 equals() 和 hashCode() 方法来实现的)
- 泛型(所有泛型类的参数都会在编译时擦除)
- 自动装箱和拆箱
- 可变长参数(在被使用的时候会创建一个数组,将传递的参数都放进这个数组中)
- 枚举(当我们使用 enum 来定义一个枚举类的时候,编译器会自动帮我们创建一个 final 类型的类继承 Enum 类,所以枚举类型不能被继承)
- 内部类(实际上会生成两个不同的 .class 文件)
- 条件编译(Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。)
- 断言(底层就是 if 语言,如果断言结果为 true,则什么都不做,否则程序抛出 AssertError 打断程序的执行)
- 数值字面量
- for-each(for-each 的实现原理其实就是使用了普通的 for 循环和迭代器。)
- try-with-resource
- Lambda 表达式
26、语法糖可能出现的坑
回答:
泛型:
public class GenericTypes {
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
}
- 当泛型遇到重载,是没办法编译通过的,比如上面的代码,因为编译之后会擦除泛型,变成原生的 List
- 泛型类型参数不能用在 Java 异常处理的 catch 语句中
- 当泛型内包含了静态变量,经过泛型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的
自动装箱和拆箱:
在 Integer 的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。因此如果不在缓存区间的比较对象使用 == 是不相等的
增强 for 循环:
Iterator
在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator
本身的方法remove()
来删除对象,Iterator.remove()
方法会在删除当前迭代对象的同时维护索引的一致性