java常见面试题2
一,基础语法
1. JDK,JRE,JVM的区别:
-
JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java 的开发环境和运行环境。
-
JRE:Java Runtime Environment 的简称,Java 运行环境,为 Java 的运行提供了所需环境。
-
JVM:java虚拟机,用于运行java字节码文件,跨平台的核心
2. 为什么java可以跨平台,实现一次编写,到处运行呢?
- 实现跨平台的核心是JVM,java引入的字节码概念,jvm只认识字节码文件。java针对不同系统有不同的jvm实现,但是被编译成字节码后都是一样的。工程师只需要面对jvm进行开发,再由jvm转译到其他系统对应api完成对应工作。
3. 常用数字类型的区别
- (最常被问到整型(说出负21到正21亿即可),记住浮点型做了解即可,整数和单精度占用4个字节,长整数和双精度占用8个字节就可以了)
在这里插入图片描述
4. Float在JVM的表达方式及使用陷阱
在这里插入图片描述 - 是相等的,针对单精度浮点型,程序里是采用科学计数法进行表示,表达为:4.2343242E7,只保留到小数点后的第七位,d1这个数字是8位的,因为单精度长度问题,最后一位的计算结果被忽略了,所以d1,d2都是7位数的情况下,他们是相等的。需要精确计算的推荐使用BigDecimal类进行计算
5. 随机数的使用
- 随机生成30~100之间的整数
//生成30~100之间的随机数
public class RandomNum {
// 方法1
public int randomNum1() {
int max = 101;
int min = 30;
return new Random().nextInt(max - min) + min;
}
// 方法2
public int randomNum2() {
int max = 101;
int min = 30;
return (int) (Math.random() * (max - min)) + min;
}
}
6. 列出1-1000的质数
/**
* 列出1-1000的质数
* 质数:在大于1的自然数中,只能被1和自身整除的数
*/
public class PrimeNum {
public static void main(String[] args) {
for (int i = 2; i <= 1000; i++) {
boolean flag = true;
for (int j = 2; j < i; j++) {
if (i % j == 0) {
flag = false;
break;
}
}
if (flag) {
System.out.println(i);
}
}
}
}
二,面向对象
1. 面向对象的三大特性是什么:
- 封装,继承,多态
2. 封装的好处:
-
实现专业的分工
-
减少代码耦合
-
可以自由修改类的内部结构
3.多态的实现机制
-
多态是三大特性中最重要的操作
-
多态是同一个行为具有多个不同表现形式或形态的能力
-
多态是同一个接口,使用不同的示例而执行不同的操作
4. 接口和抽象类有哪些异同?
- 相同点:
-
都是从具体类中抽象出的方法的声明
-
都不能被实例化
-
都可以包含抽象方法
- 不同点:
-
抽象类可以包含方法的实现,接口则只能包含方法的声明
-
继承类只能继承一个抽象类,实现类可以实现多个接口
-
抽象级别:接口>抽象类>实现类
-
作用不同:接口用于约束程序行为,继承则用于代码复用
5. 静态和实例变量(方法)的区别?
-
语法区别:静态变量前要加static关键字,实例则不用
-
隶属区别:实例变量属于某个对象的属性,调用时候需要进行实例化。而静态属于类,调用时类名.变量名即可
-
运行区别:静态变量在JVM加载类时创建,实例变量在实例化对象时创建,
-
是否被垃圾回收:静态变量在运行过程中无法被垃圾回收所释放,实例变量不再使用时就会被垃圾回收
-
存储位置:静态变量存储在JVM方法区中,实例对象存在对象堆内存中
6. Error和Exception的区别和联系
7. 类的执行顺序
-
静态优先
-
父类优先
-
非静态块优先于构造函数
执行顺序如下:142356
三,字符串
1. 请写出程序的执行结果
解析:
字符串保留在常量池中,一旦创建用final修饰
s1==s2 --> true,指向同一个内存地址
s3==s4 --> true,指向同一个内存地址
s4==s5 --> false,s2是引用类型,编译期间无法确认数值,只有运行时才能确认
s4.equals(s5) -->true,equals比较的是内容
s1==s6 -->false, s1是存放在常量池中的,s6是不在常量池进行保存的,因为保存地址不一样,所以是falase
2. String,StringBuilder,StringBuffer的区别?
答:String是把数据存放在常量池中,因此在线程池中是最安全的,但是执行速度最差,推荐少量字符串操作时候进行使用。
StringBuilder执行效率最高,但是线程不安全(线程安全:在进行多线程处理时,如果多个线程对一个对象同时操作,会不会发生异常),只推荐在单线程情况下大量字符串操作
StringBuffer执行速度在三者中排名中间,线程也是安全的。效率低的原因是更多考虑了多线程的情况,推荐多线程环境下操作
四,集合
1. List和Set的区别?
-
List允许重复,Set不允许重复
-
List允许为null,Set不允许为null
-
List是有序的,Set是无序的
-
List的常用类用ArrayList,LinkedList,Set的常用类有HashSet,LinkedHashSet,TreeSet
2. ArrayList和LinkedList的区别?
-
存储结构:ArrayList出现是作为数组的替代品,基于动态数组,在内存中所有数据都是连续的
-
LinkedList基于链表,在内存中进行松散的保存,基于指针连接在一起
-
ArrayList适用于大数据量读取,LinkedList适用于频繁新增,插入这种写操作来适用
3. HashSet和TreeSet的区别?
-
排序方式:HashSet不能保证顺序,TreeSet可以按预制规则排序
-
底层存储:HashSet基于HashMap,TreeSet基于TreeMap
-
底层实现:HashSet基于Hash表实现,TreeSet基于二叉树实现
4,TreeSet排序的编码实现
- 基于java bean来实现comparable接口,这种排序方式称为自然排序
public class Employee implements Comparable<Employee>{
private String name;
private int salary;
public Employee( String name, int salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public int compareTo(Employee o) {
return this.getSalary().compareTo(o.getSalary());
}
@Override
public String toString() {
return "Employee{" +
", name='" + name + '\'' +
", salary=" + salary +
'}';
}
}
- 实例化TreeSet的时候实现comparator接口,这种排序方式是自定义排序
public class Test {
public static void main(String[] args) {
TreeSet<Employee> employees = new TreeSet<Employee>(new Comparator<Employee>() {
public int compare(Employee o1, Employee o2) {
return (int) (o1.getSalary() - o2.getSalary());
}
});
employees.add(new Employee(3, "张三", -13001f));
employees.add(new Employee(32, "张三", -13001.1f));
System.out.println(employees);
{
}
}
}
5. hashCode()和equals()的区别
两者都是用于比较对象是否相同,但是底层算法完全不同。
-
hashCode()算法很简单,就是将当前的内存地址经过哈希演算以后返回一个整数,代表“该对象的内部地址”,所以使用hashCode()的时候生成速度非常快,但是准确性不如equals()高,小概率情况下不同对象的hashCode()是有可能相同的
-
equals()方法需要对对象中的各种因素进行考量,要进行传递性,对称性,一致性等原则,所以它非常复杂,准确性也更高,但是速度比较慢
-
总结如下:
在这里插入图片描述
五,输入输出流
1. Java IO中有几种类型的流:
输入/出流:数据链向程序进行输入/输出
字节流:二进制
字符流:可以阅读的字符
常见的流如下:
InputStream/OutputStream:顶层是抽象类,
FileInputStream/FileOnputStream:文件输入/出字节流,用于对二进制文件进行读取/写入
BufferedInputStream/BufferedOnputStream:加入缓存,提高文件的读取/写入效率
Reader/Writer:封装对字符流进行输入/输出的抽象
FileReader/FileWriter:读取/写入文本文件
InputStreamReader/OutputStreamWriter:将输入字节流转换为可读的输入字符流
BufferedReader/BufferedWriter:加入缓存,提高文件的读取/写入效率
2. Java IO中有几种类型的流:
输入/出流:数据链向程序进行输入/输出
字节流:二进制
字符流:可以阅读的字符
常见的流如下:
InputStream/OutputStream:顶层是抽象类,封装了输入/输出字节流的抽象方法
FileInputStream/FileOnputStream:文件输入/出字节流,用于对二进制文件进行读取 /写入
BufferedInputStream/BufferedOnputStream:加入缓存,提高程序的读取效率。之前FileInputStream进行文件读取的时候是读一个写一个,速度比较慢,采用这个方法可以一次性读取多个文件再进行处理,降低IO和程序处理的压力
Reader/Writer:封装对字符流进行输入/输出的抽象
FileReader/FileWriter:读取/写入文本文件
InputStreamReader/OutputStreamWriter:将输入/输出字节流转换为可读的输入/输出字符流
BufferedReader/BufferedWriter:加入缓存,提高文件的读取/写入效率
3. 利用IO实现文件复制:
编程题:复制文件到指定文件夹
public static void main(String[] args) {
File source = new File("C:/Users/86189/Pictures/clipboard.png");
File target = new File("C:/Users/86189/Pictures/clipboard1.png");
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(source);
os = new FileOutputStream(target);
byte[] b = new byte[1024];
int n = 0;
while ((n = is.read(b)) != -1) {
os.write(b, 0, n);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
六,垃圾回收与jvm内存
1. JVM的内存组成
JVM分成共享区和私有区,
-
共享区包含堆和方法区。堆用于保存程序运行时的java变量,方法区则包含了静态内容,包括静态变量,常量,类的信息,方法的声明等,这些内容是可以被所有线程可直接访问的,所以被称为共享区。
-
私有区则是指对线程来说是私有的,其他线程无法直接访问,包含程序计数器,虚拟机栈,本地方法栈。
-
程序计数器是一个行号指示器,进行程序跳转的时候,我们要记录跳转的行号是多少,方便程序进行还原。
-
虚拟机栈:包含方法执行时的状态,每个方法都在虚拟机栈形成一个栈帧,栈帧相当于一个个方法的顺时状态
-
本地方法栈和虚拟机栈最大区别是用途不同,本地方法栈用于调用操作系统级别的底层方法时才会在这里存放方法的栈帧。虚拟机栈则保存的是执行java方法时对应的栈帧。
2. 简述Java的垃圾回收机制(GC)
GC(Garbage Collection)--回收不再使用的内存
GC负责3项任务:分配内存、确保引用、回收内存
GC回收依据--当对象没有任何作用,则可以被回收。
垃圾回收是双刃剑:
1,对程序开发提供了便捷,
2,垃圾回收要对程序进行不断的监听,所以执行效率,相对于C、C++要低一些。不过随着技术发展和软硬件技术的提高,差距在不断的缩小。
垃圾回收器--使用的是有向图的方式管理和记录,堆内存中的所有对象。通过有向图可以识别哪些对象时可达的。哪些对象是不可达的。对于可达的对象进行保留,不可达的对象视为垃圾,被垃圾回收器处理掉。
3. 垃圾回收(GC)算法:常用的5种算法(扩展问题)
-
记住5个名称就好,其他内容做了解
-
引用计数算法--最简单,但效率也最低。原理是,JVM堆中保存的每一个对象都有一个被引用1计数器,当有变量引用对象时,计数器就会“+1”。当变量被释放或断开引用时,计数器就会“-1”。当计数器变成“0”则表示这个对象可以被垃圾回收。
(注意:当两个对象彼此互相引用,会形成一个循环。称为循环引用,计数器是无法解决循环引用的情况的) -
跟踪回收算法:利用JVM维护的对象引用图(可以形象的理解为JVM在内存中画了一张图,从根节点开始遍历对象的引用图。同时标记还可以遍历到的对象。遍历结束后未被遍历的对象就是没被使用的对象,就是不能使用的对象,就可以进行回收了)
-
压缩回收算法:将JVM堆中活动的对象放到一个集中的区域中,在堆的另外一端留出大块空闲区域,相当于对堆中的碎片进行处理。(对性能影响较大)
-
复制回收算法--把 堆 分成两个形同大小的区域,在任何时刻只有其中的一个区域被使用,直到其被消耗完。然后垃圾回收器会阻断程序的运行,通过遍历把所有活动的对象复制到另外一个区域中,复制时这些对象时紧密的挨在一起的。从而消除在内存中产生的碎片。复制完成后继续运行,知道区域使用完,然后重复上面的方法。
(优点:在垃圾回收的同时,也完成了对对象的重新布置和安排,因为对象都是紧密连接的所以其访问效率和寻址效率都非常高。并且一次性解决的内存碎片问题。
但是也有它自己的问题,
1,需要指定内存到小的2倍。
2,在内存调整的过程需要中断当前程序的执行,降低了程序的执行效率 -
按代回收算法(现在的主流回收算法):
如果按对象生命周期的长短进行区分,绝大多数对象的声明周期都很短。比如方法中声明的对象,方法结束这个对象就被释放了。只有很少的部分对象有较长的生命周期。例如:全局变量、一些需要一直持有易用的变量才拥有较长的声明周期。
基于上述特点,按代回收算法进行了优化,思路为--把堆分成两个或者多个子堆。每个子堆都视为一代。回收算法在运行时,优先回收年轻的对象
对于一些经过多次回收依然存活的,则把其移到高一级的堆中。
这种按代分类的做法,我们可以把一些稳定的不常用的类,减少扫描次数。进而缩小扫描范围,提升了回收的效率。
4. 请列举java中内存泄露的场景
内存泄漏--指一个不再被程序使用的对象或者变量,还在内存中占用空间。
C、C++语言中,垃圾回收是需要手动操作的,如果程序员忘记释放,就会造成内存泄漏。
JAVA--有垃圾回收机制,由回收器自动回收,可以极大的降低程序员垃圾回收的工作。但JAVA也会出现内存泄漏的场景。
常见的内存泄漏场景:
1,静态集合类,有些程序员喜欢用静态关键字“static”来修饰变量或对象。这种方法时不可取的,因为对静态对象来说,它存储在方法区中。垃圾回收器不会对“方法区”中的数据进行高频度的回收。
如果使用static 修饰了一个集合,存放的数据量又比较大。而且通常我们方法区的内存是比较小的,时间久了就会产生内存溢出的情况,进而程序崩溃。
2,各种连接--数据库拦截、网络拦截、IO拦截等,只打开,但未关闭。在JVM中这些对象一直是可达状态,因此不会被回收。时间久了就会导致程序崩溃。
3,监听器--因为其往往都是全局存在的,如果监听器中使用的对象或变量,没有进行有效的控制的话,很容易产生内存泄漏。
4,不合理的作用域--java开发中有一个基本的原则,作用域最下化。变量能声明在方法中就不要声明在方法外,能用private 不要用public。(作用域的正确使用是一个java工程师的基本功,只需要它的时候才去创建,如果一个变量的作用范围大于它的使用范围,很有可能导致内存泄漏的情况。。另外如果在平时使用时,没有吧引用对象及时设置成null,也有可能造成内存泄漏)
5. 浅复制和深复制
-
浅复制--只对对象及其变量值进行赋值,如果对象中引用了其它对象的话,那么这个对象赋值了以后,其应用对象的地址是不变的。(也就是,这两个完全相同的对象,会指向同一个引用对象)
-
深复制--不见对象及其变量值进行复制,就连引用对象也进行复制。复制的两个对象,都有自己独立的引用对象,彼此互不干扰。
package com.imooc.interview.clone;
import java.io.*;
public class Dancer implements Cloneable,Serializable{
private String name;
private Dancer partner;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dancer getPartner() {
return partner;
}
public void setPartner(Dancer partner) {
this.partner = partner;
}
public Dancer deepClone() throws IOException, ClassNotFoundException {
//序列化,将内存中的对象序列化为字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化,将字节数组转回到对象,同时完成深复制的任务
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Dancer)ois.readObject();
}
public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
Dancer d1 = new Dancer();
d1.setName("刘明");
Dancer d2 = new Dancer();
d2.setName("李敏霞");
d1.setPartner(d2);
System.out.println("Partner:" + d2.hashCode());
//浅复制(继承CloneAble接口,实现clone方法)
Dancer shallow = (Dancer)d1.clone();
System.out.println("浅复制:" + shallow.getPartner().hashCode());
//深复制(继承Serilizable接口,进行序列化和反序列化,实现deepClone方法)
Dancer deep = (Dancer)d1.deepClone();
System.out.println("深复制:" + deep.getPartner().hashCode());
}
}