Java基础面试题(持续更新)

114 阅读10分钟

JavaSE

语言特性

比较JVM、JDK、JRE

JVM是运行Java字节码的虚拟机,它使得Java拥有平台无关性的特点,因为它实现了不同操作系统的兼容性。

JRE是Java运行时环境,包括JVM、Java类库、Java命令和其他一些基础构建,但还不能用于创建新程序。

JDK是Java开发包,包括JRE和编译器和工具,能够用于创建和编译程序。

面向对象和面向过程的区别?

面向过程:性能高,但扩展性不好,难以维护。适合做一件事。

面向对象:易维护、易复用、易扩展,但性能比面向过程低。

面向对象的三大特性是什么?解释一下?

封装、继承、多态

封装:将一个对象的属性隐藏在对象内部,不允许外部对象直接访问对象的内部信息,所有属性的获取和修改必须通过统一的方法。

继承:将多个相似的类的特性抽离出来创建一个共同的父类,填入共同的属性,提高代码的重用、程序的可维护性,节省大量创建新类的时间。(比如多线程编程时继承Thread类)

多态:同一个行为具有不同表现形式的能力,常见情况就是父类或接口使用不同的子类或实现类的过程。

抽象类和接口的区别

从设计的角度来说:

  • 抽象类是对类的抽象,包括属性和行为;接口是对行为的抽象。继承是“是不是”的关系,接口是“有没有”的关系。
  • 抽象类是一种模板式设计,接口是一种辐射式设计。比如新增一个方法,抽象类只需要在抽象父类中声明定义方法即可,而接口需要在各个实现它的类中变动。

从语法的角度来说:

  • 一个类只能继承一个抽象类,一个类可以实现多个接口
  • 抽象类中的成员变量的修饰符可以多种多样,而接口只能是public static final类型的

修饰符、关键字

private、protected、public和default的区别

public:

具有最大的访问权限,可以访问任何一个在classpath下的类、接口、异常等。它往往用于对外的情况,也就是对象或类对外的一种接口的形式。

protected:

主要的作用就是用来保护子类的。它的含义在于子类可以用它修饰的成员,其他的不可以,它相当于传递给子类的一种继承的东西

default:

有时候也称为friendly,它是针对本包访问而设计的,任何处于本包下的类、接口、异常等,都可以相互访问,即使是父类没有用protected修饰的成员也可以。

private:

访问权限仅限于类的内部,是一种封装的体现,例如,大多数成员变量都是修饰符为private的,它们不希望被其他任何外部的类访问。

字符串

String str = new String("abc")语句中共创建了几个对象?

可能两个也可能一个,new String会创建一个新对象然后去常量池中找是否有该字符串,如果有则指向该字符串如果没有则在常量池中创建一个。因此如果此前创建过了,直接去常量池拿,就只创建一个对象。

讲一下String类?

String的底层结构是一个char数组,被final修饰,这保证了String的不可变性,包括String对象不可被继承,字符数组的value属性的引用地址不可修改。

final修饰String是为了高效和安全性。

高效:只有变量是不可修改的才能被缓存起来,从而实现常量池的功能。

安全:防止在调用系统级操作指令时发生变动导致系统崩溃问题。

String类是不可变的,那截取、拼接、替换的操作是怎么实现的?

通过创建新String对象的方式实现

StringBuilder和StringBuffer

StringBuilder默认的初始容量为16,在append时不会重新创建一个String对象,而是先确保数组能够放得下新添加的字符(即扩容),然后调用System的native方法将添加的字符串拷贝到字符数组中去。

StringBuffer和StringBuilder的不同主要有两点:一是线程安全的,其实就是在修改的方法上加了同步锁;二是StringBuffer的toString方法是有缓存的,多次调用new出来的String对象共享一个字符数组的内存。

泛型

讲一下Java泛型

泛型提供了编译时类型安全检测机制,本质是参数化类型,操作的数据类型被指定为一个参数。

Java的泛型是伪泛型,Java在编译期间泛型信息会被擦除。也就是说不确定的类型在编译成字节码后一定会确定具体的类型。

应用场景:构建集合工具类(存不同对象时做类似的操作)、返回通用结果(响应报文)

异常

Java异常有哪些?

image.png

Exception是程序本身可以处理的异常,可以用try……catch来捕获

Error是程序无法处理的错误,不建议通过catch捕获,JVM一般选择线程终止

受检异常在编译过程中没有捕获会报错,不受检查异常即使不处理也可正常通过编译

反射

Java的反射有什么好处?坏处?

好处:

  1. 反射赋予了jvm动态编译的能力。动态编译可以最大限度的体现Java的灵活性。

坏处:

  1. 反射设计动态解析的类型,无法用jvm优化,因此性能开销比非反射操作要大
  2. 破坏封装性,忽略权限检查,因此可能会破坏封装性而导致安全问题。

Java反射的使用场景

  1. 无法使用new生成一个对象,比如调用的是来自网络的二进制.class文件,而没有其.java代码
  2. 与注解结合完成调用注解解释器,执行行为
  3. 延时加载,有的类可以等到用到时再动态加载到jvm中,可以动态的加载需要的对象(有点像多态,运行时决定具体的实现类)
  4. 避免将代码写死,开发通用框架时就常用反射,为了保证框架的通用性,他们可能需要根据配置文件加载不同的对象或类、调用不同的方法,如果写死则不能做到这一点

其他

Java参数传递是传值还是传引用?

Java中只有值传递。

如果参数是基本类型,传递的就是基本类型的字面量值的拷贝,会创建副本。

如果参数是引用类型,传递的就是实参所引用的对象在堆中的地址值的拷贝,同样也会创建副本。

说说hashCode()和equals()之间的关系?

  1. 只要重写equals,就必须重写hashCode
  2. Set和Map的key需要用自定义对象时,必须重写hashCode和equals

当自定义类不会创建对应的散列表时,这两个方法没有一点关系。

当会创建类的对应散列表时,两个方法有关系:

  • 如果两个对象相等那么他们的hashcode值一定相同
  • 如果两个对象hashcode相同,他们不一定相等(哈希冲突)

判断两个对象是否相等,必须要重写hashCode方法,因为两个对象即使相等,散列值却很难相等。

成员变量与局部变量的区别

  1. 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  2. 从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  3. 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  4. 从变量是否有默认值来看,成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

Java集合

讲一下HashMap

底层实现:数组+链表+红黑树

默认初始容量是16,扩容因子为0.75,每次扩容2倍

HashMap的put过程

计算需要存储的对象的哈希值(数组下标)将对象放到数组对应位置上,如果发生了哈希冲突,则按链表的顺序往下(jdk1.8是尾节点插入,jdk1.7以前是头节点插入)依次存储即可,8树化6链化可以减少频繁变化造成的开销。

计算hashcode是如何实现的?

为了使散列值尽可能离散,采用的方法是hashcode的高16位和低16位进行异或运算,得到的结果再与当前数组长度减一进行与运算。这也是为什么当前长度要控制在2的n次方的原因。

jdk1.8计算hash时进行了两次扰动(运算次数)

jdk1.7计算时进行了九次扰动(四次位运算、五次异或运算)

HashMap扩容时的元素如何存放?

jdk1.7是创建一个两倍长度的数组再把每一个原数组、链表中的键值对重新进行哈希运算放入新数组里

jdk1.8做了改进,新下标的位置只能在原下标位置或者原下标位置+原容量(这是由哈希表容量是2的n次方保证的)

HashMap使用红黑树而不是其他结构的原因?

首先和二叉搜索树比较,二叉搜索树在极端情况下会退化成链表导致读取效率低下

其次和AVL树比较,AVL树保证完全平衡,读取性能高但维护起来稍慢,红黑树不追求完美平衡,任何增删操作的修改维护可以在3次旋转内完成,只是读取上略逊于AVL树

HashMap在线程不安全的情况下会发生什么问题?

  1. jdk1.7前可能会产生链表中死循环的可能,主要原因是头插法
  2. jdk1.8以后解决了死循环问题,但仍有可能造成数据丢失问题

ConcurrentHashMap的实现原理

HashTable的思路是锁住整个表,导致效率非常低下,而另外一种线程安全的ConcurrentHashMap就通过锁分段技术提高并发效率

jdk1.7以前是基于分段锁(做两次hash,第一次放到一个段中,第二次再映射到段中某个具体的位置)

jdk1.8摒弃了分段锁的概念,锁的是首节点。直接用Node数组+链表+红黑树实现,Node数据结构包括了哈希值、键、值、下一个Node,用volatile修饰保证并发的可见性。在put过程中,首先得到Node对应下标,CAS尝试修改该节点,失败则自旋,成功则使用同步内置锁来锁住链表或树首节点