Java面试系列 — 基础篇(一)
先整理出一批面试笔试面试题。后续将继续更新,如果本文中出现问题,请及时与蛐蛐联系,蛐蛐马上继续修改,后续也会同步更新。
回答问题原则:
- 首先,要保证回答问题的条理性。列出来1、2、3、4进行回答,否则会让面试官感觉你没有逻辑。
- 回答问题时,尽量要引导面试官走入到自己的节奏。例如:当面试官问道HaskMap的底层原理时,我们该如何回答?首先我们要知道在1.7和1.8版本的HashMap的底层原理是不一样的,在1.7中,HashMap的底层是有数组+链表构成,在1.8中,是由数组+链表+红黑树构成,如果我们对红黑树不了解,就不要贸然回答,可以只回答数组+链表,否则,面试官一般会根据你回答的问题继续深入问下去。
项目介绍
- 知道项目是做什么的
- 知道项目的功能。
- 知道用到哪些技术。
- 在这个项目的承担角色
- 通过这个项目有哪些技术成长
Java跨平台原理(字节码文件、虚拟机)
- C语言都会直接编译成平台的机器码,如果想要平台,需要编译成一定平台的机器码(C语言比Java速度快的原因)
- Java文件(.java)首先需要编译成与平台无关到字节码文件(.class),然后通过Java虚拟机编译成机器码在各个平台运行。
- Java语言具有一次编译,到处运行的提点。
Java的安全性
- Java取消了危险的指针(虽然指针的功能很强大),使用更为安全的引用。
- Java自己具有垃圾回收机制,不需要程序员在进行垃圾回收,这样避免了忘记及时回收而导致的内存泄漏。
- 异常处理机制和强制类型转化。
高级特性 | 低级特性 | 优点 | 来源 |
---|---|---|---|
平台安全性 | Java 编译器和虚拟机强制实施的内置的语言安全特性:1.强大的数据类型管理。2.自动内存管理3.字节码验证4.安全的类加载 | 为应用程序开发和运行提供一个安全平台。编译时数据类型检查和自动内存管理可使代码更健壮,减少内存损坏和漏洞。字节码验证可确保代码符合 JVM 规范并防止恶意代码破坏运行时环境。类加载器可防止不受信任的代码干扰其他 Java 程序的运行。 | 官方文档 |
什么是JVM?什么是JDK? 什么是JRE?
- JVM:JVM是Java Virtual Machine(Java虚拟机)的缩写。它是Java实现跨平台特性的核心。首先我们知道所有的Java文件会被解析成class字节码,而字节码的运行环境就是JVM。
- JRE是java runtime environment(java运行环境)的缩写。光有JVM还不能让class文件执行,因为在运行class的时候需要调用类库lib。而jvm和lib和起来就称为jre。
- JDK:JDK是java development kit(java开发工具包)的缩写。
- 三者关系:JDK是Java开发时候必须要用的开发工具包,而JDK中包含JRE,JRE中又包含JVM。
Java语言是一种强类型的语言
任何变量必须指定其类型
Java的注释的方式
Java注释共分为单行注释、多行注释和文档注释。
- 单行注释:采用‘//’ 的形式。
- 多行注释:采用‘/.../’的形式。
- 文档注释,采用‘/*.../’的形式。
逻辑运算符
&与、|或、!非、&&短路与、||短路或
条件运算符
格式:(条件)?表达式1:表达式2
Java中的唯一的一个三目运算符
基本数据类型及其字节数
数据类型 | 关键字 | 字节数 |
---|---|---|
整数性 | byte | 1 |
整数性 | short | 2 |
整数性 | int | 4 |
整数性 | long | 8 |
浮点型(单精度) | float | 4 |
浮点型(双精度) | double | 8 |
布尔型 | boolean | 1 |
字符型 | char | 2 |
i++ 和 ++i 的异同之处
共同点:
- 最终结果都是让变量i自增,等价于i=i+1。
- 都是针对于变量。
不同点:
i++:先运算,在自增
++i:先自增,在运算
public static void main(String[] args) {
// int index = 0;
// System.err.println("index++ :" + index++); //结果是0
int index = 0;
System.err.println("++index :" + ++index); //结果是1
}
&和&&的区别和联系
&和&&的共同点:
- &和&&都可以作为逻辑运算符。
&和&&的区别:
- 对于&&:当&&左边的操作数为false或左边表达式结果为false时。&&右边的操作数或表达式将不参与计算,此时最终结果都为false。
- 对于&:无论任何情况,&两边的操作数或表达式都会参与计算
- &还可以用作位运算符。
综上所述:当作为逻辑运算符,多个表达式时,尽量使用&&,当其中一个表达式出现问题时,将不会继续运算。
用最有效率的方法算出4乘以4等于多少?
使用位运算效率最高,位运算是操作二进制来计算,最一个数进行左移,例如:4 << 2 ,此时相当于4 * 2^2, 即一个数左移n位,就相当于乘以了2的n次方。
基本数据类型的类型转换规则
类型转换.png实心箭头代表无精度损失转换,空心箭头转换可能精度损失。
流程控制结构有几种?分别是哪几种?
三种,分别是顺序结构、分支结构和循环结构。
if多分支语句和switch多分支语句的异同之处
相同之处:都是分支语句,对超过一种情况进行判断处理。
不同之处:
- switch更适合多分支情况,即有很多中情况需要判断,但是判断条件单一,且各分支执行结束条件为break。而if-elseif-else 多分支主要适用于分支较少,但是判断类型不单一且当前分支执行完,后面的分支不再执行。
- switch为等值判断,而if等值和区间都可以。
while和do-while循环的区别
while:先判断条件,如果符合条件才会执行。
do-whie:先执行后判断,至少执行一次。
break和continue的作用
break:结束当前循环,并推出当前循环体。
continue:结束当前循环,继续下次循环。
带标签的break语句
- 什么是标签
- 它是一个合法的标识符和一个冒号组成的字符系列
- 标签的作用
- 用来给一段代码做标记
- 带标签的break的作用
- 用来结束指定的循环
- 使用条件
- 一定发生了嵌套循环
- 标签在外循环
- 带标签的break在内循环中
带标签的continue请参考带标签的break
请使用递归算法计算n!
public static void main(String[] args) {
System.out.println(factorial(6)); //输出结果为720
}
public static int factorial(int n) {
if (n == 1 || n == 0) {
return n;
} else {
return n * factorial(n - 1);
}
}
递归的定义和优缺点
程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
特点:
- 递归就是在函数里调用自身。
- 在使用递归时,必须要有一个明确的结束条件。
- 代码简洁,但是效率低,一般不建议使用递归。
- 在递归调用的过程中,都要将相关数据要全部压栈保存起来以防止丢失,如果递归过深,会导致栈内存溢出。
数组的特征
- 数组是相同类型的数据的有序集合
- 数组会在内存在开辟出一块连续的空间。
- 索引从0表示
- 数组元素是有序的(索引顺序)
- 数组既可以存储引用数据类型,又可以存储基本数据类型
- 数组的长度是固定的。
- 数据的元素都默认值。
数组内存分配
数组是引用数据类型
- 栈内存:存放数组名、局部变量,方法执行完立即释放空间,先进后出的结构。
-
堆内存:存放数组元素的内容,每个元素都有默认值,方法执行完毕后不会立即释法内存,由垃圾回收器负责。
数组内存.png
冒泡排序
- 思想:比较相邻的两个元素,将值大的元素放到右面。
public static void main(String[] args) {
int[] arr = {6, 3, 8, 2, 9, 1};
System.out.print("排序前数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
//冒泡排序的核心,两个for循环
for (int i = 0; i < arr.length - 1; i++) {//外层循环控制排序趟数
for (int j = 0; j < arr.length - 1 - i; j++) {//内层循环控制每一趟排序多少次
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
System.out.println();
System.out.printf("排序后的数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
}
插入排序
- 思想:每一步,将一个带排序的记录,插入到前面已经排好顺序的序列中。
public static void main(String[] args) {
int[] arr = {6, 3, 8, 2, 9, 1};
System.out.print("排序前数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
//插入排序的基本思想
for (int i = 1; i < arr.length; i++) {
int j = i;
while (j > 0 && arr[j] < arr[j - 1]) {
arr[j] = arr[j] + arr[j - 1];
arr[j - 1] = arr[j] - arr[j - 1];
arr[j] = arr[j] - arr[j - 1];
j--;
}
}
System.out.println();
System.out.printf("排序后的数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
}
选择排序
- 思想:每一次从待排序的数据元素中选择最大(最小)的一个元素作为首元素,一直到所有元素全部排完。
public static void main(String[] args) {
int[] arr = {6, 3, 8, 2, 9, 1};
System.out.print("排序前数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
//选择排序的基本思想
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
//进行交换,如果min发生变化,则进行交换
if (min != i) {
arr[min] = arr[min] + arr[i];
arr[i] = arr[min] - arr[i];
arr[min] = arr[min] - arr[i];
}
}
System.out.println();
System.out.printf("排序后的数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
}
快速排序
- 思想:每次排序都找一个初始值,以初始值为分界,使得初始值左侧的数值都小于初始值,初始值右侧的数值都大于初始值,然后递归该过程,直到排序结束。
public static void main(String[] args) {
int[] arr = {6, 3, 8, 2, 9, 1};
System.out.print("排序前数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
recursion(0, arr.length - 1, arr);
System.out.println();
System.out.printf("排序后的数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
}
//第一步,查找一个初始值
public static int getValue(int left, int right, int[] arr) {
//获取一个初始值
int index = arr[left];
while (left < right) {
//从右向左查找
while (left < right && arr[right] >= index) {
right--;
}
if (left < right) {
//小于初始值的移到左侧
arr[left] = arr[right];
}
//从左向右查找
while (left < right && arr[left] <= index) {
left++;
}
if (left < right) {
//大于初始值的移到右侧
arr[right] = arr[left];
}
}
//初始值位置不再变化时
arr[left] = index;
return left;
}
//第二步,采用递归的方式处理初始值左右两堆数据
public static void recursion(int left, int right, int[] arr) {
int index;
if (left < right) {
//获取初始值
index = getValue(left, right, arr);
//对小于初始值的那堆数据进行递归排序
recursion(left, index - 1, arr);
//对大于初始值的那堆数据进行递归排序
recursion(index + 1, right, arr);
}
}
二分法查找
算法:当数据量很大适宜采用该方法。采用二分法查找时,数据需是排好序的。主要思想是:(设查找的数组区间为array[low, high])
(1)确定该区间的中间位置K
(2)将查找的值T与array[k]比较。若相等,查找成功返回此位置;否则确定新的查找区域,继续二分查找。区域确定如下:a.array[k]>T 由数组的有序性可知array[k,k+1,……,high]>T;故新的区间为array[low,……,K-1]b.array[k]<T 类似上面查找区间为array[k+1,……,high]。每一次查找与中间值比较,可以确定是否查找成功,不成功当前查找区间缩小一半,递归找,即可。时间复杂度:O(log2n)。
public static void main(String[] args) {
int[] arr = {134, 635, 85, 2, 654, 13, 99, 83, 423, 873, 109, 888, 457, 1095, 2048};
int word = 457; //所要查找的数
System.out.println("普通循环查找" + word + "的次数是" + genetalLoop(arr, word));
System.out.println("二分法查找" + word + "的次数是" + binarySearch(arr, word));
}
//正常情况下,for循环遍历数组,直到找到该数据
public static int genetalLoop(int[] arr, int word) {
//普通的循环法,最少需要比较一次,比如查找1,最多需要比较15次,比如8721
int count = 0;
for (int i = 0; i < arr.length; i++) {
count++;
if (word == arr[i])
break;
}
return count;
}
//二分法查询
public static int binarySearch(int[] arr, int searchWord) {
Arrays.sort(arr); //先对传进来的数组进行排序
System.out.println("排序后的数组:" + Arrays.toString(arr));
//二分法查找
int index = 0;
int s = 0;
int e = arr.length - 1;
int count = 0;
for (int i = 0; i < arr.length / 2; i++) {
count++;
index = (s + e) / 2;
if (arr[index] < searchWord) {
s = index;
} else if (arr[index] > searchWord) {
e = index;
} else {
break;
}
}
return count;
}
可变参数的作用和特点
- 可变参数只能是形参。
- 可变参数只能有一个,而且必须是最后一个。
- 方便、简单、减少重载方法的数量。
数组做形参和可变参数做形参的区别和联系
- 联系
- 实参都可以书数组
- 区别
- 个数不同
- 位置不同
- 实参不同
面向过程和面向对象的优缺点
- 面向过程
优点:性能比面向对象高。
缺点:没有面向对象容易维护、易扩展、易复用
- 面向对象
优点:易维护、易复用、易扩展
缺点:性能比面向过程低
为什么面向过程性能更高?
因为类调用时需要实例化,开销比较大,比较消耗资源
方法重载和方法重写的区别
-- | 英文写法不同 | 位置不同 | 作用不同 |
---|---|---|---|
重载 | overload | 同一个类中 | 在同一个类中为同一个行为提供多个实现方式 |
重写 | override | 子类和父类 | 父类无法满足子类的要求,子类通过重写父类的方法来满足需求 |
-- | 修饰符 | 返回值 | 方法名 | 参数 | 异常 |
---|---|---|---|---|---|
重载 | 无关 | 无关 | 相同 | 不同 | 无关 |
重写 | 大于等于 | 小于等于 | 相同 | 相同 | 小于等于 |
局部变量和成员变量的区别
- 定义的位置
- 成员变量:类中定义的变量
- 局部变量:在方法块中定义
- 内存中的位置
- 成员变量:堆内存中
- 局部变量:栈内存中
- 是否有默认值
- 成员变量:有默认值
- 局部变量:没有默认值
- 作用范围
- 成员变量:当前类的非静态方法
- 局部变量:当前方法
- 存在时间
- 成员变量:
- 局部变量:
静态变量和非静态变量的区别
-- | 数量 | 分配空间的时间 | 调用方式 | 存储位置 |
---|---|---|---|---|
静态变量 | 所有对象只有一个,影响所有的对象 | 第一次加载类的时候 | 通过类名直接调用 | 方法区 |
非静态变量 | 每个对象一个,只影响当前对象 | 创建对象时分配时间 | 创建对象,通过对象名调用 | 堆内存 |