1.JDK的组成
JRE(Java Runtime Environment):Java 运行时环境
JDK 内置完整的 JRE,是运行 Java 程序的基础,包含:
JVM(Java Virtual Machine):Java 虚拟机,核心组件,负责将字节码(.class 文件)解释 / 编译为本地机器码执行,实现 “一次编写,到处运行”。
核心类库(rt.jar 等):Java 基础 API,如
java.lang(Object、String、Integer)、java.util(集合)、java.io(IO 操作)等,是编写 Java 程序的基础。
开发工具(位于 JDK 的 bin 目录下)
这是 JDK 区别于 JRE 的核心,是开发阶段的关键工具,面试需重点列举核心工具:
javac:Java 编译器,将.java源文件编译为.class字节码文件。java:Java 运行工具,启动 JVM 并执行字节码文件。
2.开发java程序的步骤
(1)编写 Java 源文件(核心编码阶段)
使用文本编辑器(Notepad++)或 IDE(IntelliJ IDEA/Eclipse)编写代码,保存为
.java后缀的源文件;
(2)编译源文件(javac 编译)
使用 JDK 自带的
javac命令(或 IDE 自动编译)将.java源文件编译为 JVM 可识别的 字节码文件(.class);
(3)运行字节码程序(java 执行)
使用 JDK 自带的
java命令(或 IDE 运行按钮)启动 JVM,加载并执行.class字节码文件;
3.二进制转换为其他进制的方法
(1)二进制转换为十进制:每一位乘以对应2的次幂数
eg: 101=> 1*2的2次幂 + 0 2的1次幂 + 1 2的0次幂 = 3
(2)二进制转换为八进制:每3位二进制作为一个单元,最小数是0,最大数是7,0-7有8个数字,
eg: 01100001--->001,100,001---->141
(3)二进制转换为十六进制:每4位二进制作为一个单元,最小数是0,最大数是15
eg: 01100001 --->0110,0001-> 61
eg: 11111010 --->1111,1010-> FA
扩展:十进制转二进制呢?答:除2取余法(逆向排列)
4.说一说java中基本类型有哪些?
(1)字节byte 占1个字节,范围:-128~127
(2)短整型short 占2个字节,范围:-3万多~3万多
(3)整型int,注意写的数字默认就是整型, 占4个字节 范围:-21亿~21亿(10位数,大概42亿左右)
(4)长整型long,8个字节,范围:19位数字,结尾要写L或l结尾,推荐L
(5)单精度浮点数(小数)float,占4个字节,结尾必须是f或F,推荐F,注意这个类型不精准,精度为8位,超过会采用科学计算法或小数点后面舍去
(6)双精度浮点数(小数)double,占8个字节,也可以不写,默认小数就是double,结尾用D或d,推荐D 精度超过17位,超过会采用小数点后面四舍五入或科学计算法
(7)char字符类型,占2个字节,0-65535
(8)boolean布尔类型,占1个字节,true或false
5.方法可以重载
介绍:一个类中,出现多个方法的名称相同,但是它们的形参列表是不同的,那么这些方法就称为方法重载了。
注意:方法重载只关注参数类型不同、类型的个数不同、类型的顺序不同,与返回值、修饰符和参数名没有关系
6.什么是强制类型转换?以及什么时候需要用到强制类型转换?
首先,Java 中的强制类型转换,简单说就是主动将一种数据类型转换成另一种指定数据类型,语法上需要用 () 把目标类型括起来,放在要转换的变量 / 值前面。
它主要用在「大范围数据类型向小范围数据类型转换」的场景(也叫「向下转型」),因为 Java 只会自动完成「小范围向大范围」的转换(自动类型转换 / 向上转型,比如 int 转 double),反过来不会自动完成,必须手动强制转换。
举个简单例子:double 类型的数值 3.14 要转换成 int 类型,就不能直接赋值,必须写 (int) 3.14,这就是强制类型转换。
7.基本数据类型的强制转换可能会出现哪些问题?
基本数据类型的强制转换,主要会出现两个核心问题:精度丢失和数据溢出。
第一个问题是「精度丢失」,主要出现在浮点型转整型的时候。因为浮点型包含小数部分,强制转换成整型后,会直接舍弃小数部分(不是四舍五入),只保留整数部分。比如 double 类型的 5.99,强制转换成 int 就是 (int) 5.99,结果是 5,小数点后面的 .99 直接丢失了,这就是精度丢失。
第二个问题是「数据溢出」,主要出现在同类型但取值范围不同的整数之间转换(比如 long 转 int)。因为小范围类型存不下大范围类型的数值,会出现数值错乱。比如 long 类型的 2147483648(这个数比 int 的最大值 2147483647 大 1),强制转换成 int 后,结果会变成 -2147483648,出现数值溢出错乱。
还有一个小细节,布尔类型(boolean)不能和任何其他基本数据类型进行强制转换,这是 Java 语法不允许的。
面试官可能的追问:如何避免整数强制转换时的溢出问题?
基础阶段可以先判断大范围数值是否在小范围类型的取值范围内,再进行转换;进阶阶段可以使用 Integer.MAX_VALUE、Integer.MIN_VALUE 这些来做判断,比如先判断 long 数值是否小于等于 Integer.MAX_VALUE 且大于等于 Integer.MIN_VALUE,再转 int
8.JVM内存分布介绍
方法区(元空间):存放类(字节码文件)信息、常量,静态变量。
栈:存放正在执行的方法以及方法中定义的变量
堆:存储new出来的内容,例如:对象、数组
本地方法栈:存放native本地方法,和C语言对接。
程序计数器:记录程序执行的位置(地址)
9.Java 中的数组是什么?它的本质是什么?
Java 数组是一种固定长度、存储相同数据类型元素的容器。
本质:数组是一个引用数据类型(对象),存储在 JVM 的「堆」中(数组变量名存储在栈中,指向堆中的数组对象)。
关键特性:
长度固定:一旦初始化完成,长度无法修改(想要动态扩容,需手动创建新数组并复制元素,如
ArrayList的底层实现)。元素类型统一:只能存储声明类型的元素(基本数据类型或引用数据类型),不能混合存储。
索引从 0 开始:通过索引快速访问元素,访问时间复杂度为 O (1)。
10.Java 数组的初始化方式有哪几种?
静态初始化:直接指定数组元素,数组长度由元素个数隐式确定,无需手动指定长度。
动态初始化:只指定数组长度,元素由 JVM 赋予默认初始值,后续可手动修改。
11.Final 有什么用?
被final修饰的类不可以被继承
被final修饰的方法不可以被重写
被final修饰的变量不可以被改变,
被final修饰不可变的是变量的引用,而不是引用指向的内容, 引用指向的内容是可以改变的
12.重写和重载的区别?
定义
重写是当子类提供了父类中某个方法的具体实现时,就称为重写,重写允许子类修改继承自父类的方法的行为,以便更符合子类的需求
重载是指同一个类中可以定义多个方法,它们具有相同的方法名称但参数列表不同(参数的数量、类型或顺序可以不同),重载允许在同一个类中使用相同的方法名称来执行不同的操作
方法签名
重写的方法签名必须完全相同,包括方法名称和参数列表
重载方法签名必须不同,即方法名称相同但参数列表不同
多态
重写体现了运行时多态,允许子类重定义父类的方法,以便在运行时选择正确的方法实现
重载体现了编译时多态,通过相同的方法名称和不同的参数列表,允许编译器在编译时选择合适的方法
访问修饰符
子类重写父类的方法时,访问修饰符不能比父类的方法更严格。例如,如果父类方法是public,那么子类重写的方法也必须是public或protected,不能是private
重载方法的访问修饰符可以是任何有效的修饰符,与其他方法的访问级别无关
返回类型
重写的方法可以改变返回类型,但只能返回与父类方法相同类型或其子类类型的返回值
重载方法可以有不同的返回类型,返回类型与参数无关
13.==和equals的区别?两个都重写的情况,equals相等hashcode一定相等吗?hashcode相等equals一定相等吗?什么场景需要同时重写equals和hashcode方法?具体怎么重写equals方法和hashcode方法?
==和equals的区别?
==比较基本数据类型时比较值,比较引用数据类型时比较内存地址
首先equals是来自于Obejct类,如果子类没有重写equals方法,那就继承Object类中equals方法里边的逻辑,也是用==来比较的,如果是子类自己重写了equals方法,一般是比较两个相同类型的对象的属性值是否一一相等,而且由于equals方法来自于Object类,所以只有是引用数据类型才有equals方法,基本数据类型没有equals方法
两个都重写的前提下,equals相等hashcode一定相等吗?hashcode相等equals一定相等吗?
equals相等时,hashcode一定相等,hashcode相等时,equals不一定相等,因为有可能出现hash冲突,有可能不同的对象中的属性值不相等,但是算出来的hashcode是一样的
什么场景需要同时重写equals和hashcode方法?
首先如果只是为了比较两个相同类型的对象中的属性值是否一一相等,其实只需要重写equals方法然后调用比较即可,hashcode方法不一定要重写,但是如果你只重写了equals方法而没有重写hashcode的话,有一种场景会出现问题,就是万一这个对象被作为hashmap的key,这时候hashmap的底层比如put方法要判断这个key是否已存在,不存在就继续追加链表末尾或者加入新的红黑树节点,如果存在则走更新value的逻辑,其底层会先判断hashcode是否相等,相等再判断equals方法是否相等,如果都相等才是真正的相等,这时候就会出问题了,所以一般建议重写了equals方法也要重写hashcode方法以防万一最为保险
补充:
(1)没有只重写hashcode方法而不重写equals方法的场景
(2)为什么hashmap底层会先判断hashcode方法再判断equals方法?因为hashcode的值是在这个对象在内存中创建好之后就会计算好,后续调用hashcode方法的时候相等于只需读取这个内存值就可以了,非常快,不需要重新计算,而equals方法则需要一个个属性值比较,比较耗时,而且两个相同类型的对象中的属性值要是真的全是相同,不可能计算出来的hashcode不相同,这样的设计可以让我们快速地刷选走连hashcode都不相同而对象的属性值不可能相同的情况,这样就可以少执行很多耗时的equals方法了
具体怎么重写equals方法和hashcode方法?
重写equals方法
检查对象是否是自身引用,可以用==来判断,如果是直接返回true
检查对象是否为null或者类型不匹配,如果是返回false
接下来逐个调用每一个属性执行equals方法进行比较,只要有一个出现false,那就直接返回false,如果全部equals方法的执行都为true,则返回true
重写hashcode方法
原理相当于利用对象中的各个属性进行参与计算,比如相当于会调用所有属性的hashcode的方法进行加减乘除计算得出一个hashcode,而hashcode的计算方式是可以自定义的,没有统一标准,hashcode方法也是来自于Object类的,Object类的实现底层调用了一个native方法,底层是利用该对象的内存地址参与运算的,反正不管用哪种计算方式最终返回一个int类型范围的数字即可,所以hashcode是有可能是负数的,而且注意由于hashcode方法来自于Object类,所以只有引用数据类型才有hashcode方法,基本数据类型没有hashcode方法
补充:在重写equals方法和hashcode方法的过程中,如果对象内部有某一个属性没有重写equals方法和hashcode方法,要一直往里边嵌套重写到所有的子属性都有重写equals方法和hashcode方法为止
14.Integer i1 = 127;Integer i2 = 127;i1==i2?Integer i1 = 127;Integer i2 = 127;i1.equals(i2)?Integer i3 = 128;Integer i4 = 128;i3==i4?Integer i3 = 128;Integer i4 = 128;i3.equals(i4)?int i5 = 128;Integer i6 = 128;i5==i6?Integer i = 128和Integer i = new Integer(128)有什么区别?
jdk开发者对于主流的几个基本数据类型的包装类都提供了对象池的缓存功能,目的是为了复用高频率对象,减少重复创建高频率对象的内存占用和时间开销,Integer会缓存值范围从-128到127之间对象,至于为什么是-128-127?应该是jdk开发者经过统计认为在项目中这个范围的数字出现的概率最高
Integer i1 = 127;Integer i2 = 127;i1==i2?
true
Integer i1 = 127;Integer i2 = 127;i1.equals(i2)?
true
Integer i3 = 128;Integer i4 = 128;i3==i4?
false
Integer i3 = 128;Integer i4 = 128;i3.equals(i4)?
true
int i5 = 128;Integer i6 = 128;i5==i6?
true,当一个int类型和一个Integer类型使用==进行比较时,Integer对象会进行自动拆箱操作,转换为int类型,然后==就是比较基本数据类型的值
Integer i = 128和Integer i = new Integer(128)有什么区别?
Integer i= 128底层相当于会自动调用Integer类的value0f()方法,这个value0f()方法底层会先判断数字范围是否在对象池范围内,是就复用对象池对象,不在范围内就会走new Integer(128)
Integer i = new Integer(128)底层相当于每次调用new都会在堆内存中开辟一块新的空间创建一个新的对象
15.java常见的集合类有哪些?
Map接口和Collection接口是所有集合框架的父接口:
Collection接口的子接口包括:Set接口和List接口
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及 Properties等
Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
16.接口和抽象类的区别?
从几个维度来对比
实现方面
抽象类的子类可以使用extends来继承父类
接口的实现类要使用implements来实现接口
构造函数
抽象类可以有构造函数,但是目的不是为了自身实例化,作用是为了子类实例化的时候可以初始化父类的成员属性
接口没有构造函数
main方法
抽象类可以有main方法
接口没有main方法
实现数量
由于抽象类是一个类,所以一个类只能单继承一个抽象类
一个类可以实现多个接口,而且接口可以多继承接口
成员变量
抽象类的成员变量可以有不同的访问修饰符
接口只能包含public static final类型的常量
使用场景
抽象类适合复用代码和成员变量的场景
接口适合定义抽象行为的场景
17.说一下java面向对象的三大特性?
封装
封装是将对象的属性和行为包装在类中,通过private修饰保护对象属性,对外提供public修饰的方法来控制数据访问
继承
继承是通过extends继承父类属性和方法,起到减少重复代码,提高复用性的作用
多态
多态是通过接口/抽象类定义规范,不同子类或者实现类可以重写或者实现同一方法,表现出不同行为,可以降低代码耦合度,提高代码可维护性
18.说一下浅拷贝和深拷贝的区别,怎么实现浅拷贝?怎么实现深拷贝?
说一下浅拷贝和深拷贝的区别
首先说说拷贝就是复制嘛,就是以一个对象为模版,复制出另外一个对象
浅拷贝:修改了复制出来的对象中成员属性,会影响原对象的成员属性
深拷贝:修改了复制出来的对象中成员属性,不会影响原对象的成员属性
怎么实现浅拷贝?
有好几种方案
new一个新对象,然后调用set方法赋值,原对象使用get方法取值,这种最常规
使用构造方法进行属性初始化,原对象使用get方法取值
比如使用主流的spring和hutool工具都有提供bean的转换工具
实现Cloneable接口,调用clone方法
怎么实现深拷贝?
重写clone方法,重写的逻辑说白就是要重新创建成员属性的值来进行赋值,使用反射或者new都可以,反正就是要完全重新创建成员属性的值,而不能依赖java的值传递机制,这样就算修改了复制对象的成员属性的值,也不会影响到原对象的成员属性的值了,接下来就可以调用clone方法了,注意这个需要递归实现,意思就是对象的属性中可能也是个引用类型的对象,要一直要往里层重写clone方法,这个其实就和重写equals方法和hashcode方法的原理类似
序列化和反序列化,这种方案不太推荐,因为性能一般
补充:
(1)java中只有值传递没有引用传递,基本数据类型的值就是本身这个值,而引用数据类型的值说白就是指向对象的内存地址,而浅拷贝就是利用了java的值传递来进行赋值的
(2)注意clone方法是来自于Object类的一个native方法,底层其实就是走java的值传递机制
(3)注意没有重写Object类的clone方法的前提下,Cloneable接口是必须要实现的,Cloneable接口中其实没有任何的抽象方法,说白实现Cloneable接口可以理解成是当做一个标志,只有实现了Cloneable接口才能调用clone方法,如果不实现Cloneable接口直接调用clone方法会报错的,当然如果已经重写了clone方法,那可以不用实现Cloneable接口,所以深拷贝不需要一定要实现Cloneable接口,而浅拷贝需要一定要实现Cloneable接口
(4)注意数组是不需要实现Cloneable接口的,可以直接调用clone方法,当然数组就只有浅拷贝了,没有深拷贝的说法了,因为数组没办法重写clone方法
(5)注意使用clone方法创建对象和反序列化创建对象是不会触发构造方法的,new和反射创建对象是会触发构造方法的
(6)clone方法体现了设计模式中的原型设计模式的思想