Java基础常见面试题总结(中)

106 阅读5分钟

面向对象基础

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

创建一个对象用什么运算符?对象实体与对象引用有何不同

对象的相等和引用相等的区别

类的构造方法的作用是什么?

如果一个类没有声明构造方法,该程序能正确执行吗?

如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了

因此如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。

构造方法有哪些特点?是否可被 override?

不可override,但可以overload

面向对象三大特征

封装

继承

多态: 父类的引用指向子类的实例

接口和抽象类有什么共同点和区别?

共同点

  • 都不能被实例化。
  • 都可以包含抽象方法。
  • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

区别

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  • 一个类只能继承一个类,但是可以实现多个接口。
  • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

深拷贝和浅拷贝区别了解吗?什么是引用拷贝?

image.png

Java常见类

Object

== 和equals()的区别

== 对于基本类型和引用类型的作用效果是不同的:

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。

Integer中的equalsString中的一样,也是被重写过,比较的是拆箱后的值

为什么要有hashCode?

为什么重写 equals() 时必须重写 hashCode() 方法?

image.png

String

String、StringBuffer、StringBuilder 的区别?

  • 可变性

    String是不可变的

    StringBuilder与StringBuffer都继承自AbstractStringBuilder类,AbstractStringBuilder类提供了很多修改字符串的方法

  • 线程安全性

    String中的对象是不可变的,也就可以理解为常量,线程安全。

    AbstractStringBuilderStringBuilderStringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacityappendinsertindexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

  • 性能

    每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

String 为什么是不可变的?

image.png

被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。

但final关键字修饰的数组保存字符串并不是String不可变的根本原因,因为这个数组保存的字符串是可变的(final修饰引用类型变量的情况)

String 真正不可变有下面几点原因:

  1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

字符串拼接用“+” 还是 StringBuilder?

Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。

字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象:

    String[] arr = {"he", "llo", "world"};
    String s = ""; 
    for (int i = 0; i < arr.length; i++) {
        s += arr[i]; 
    } 
    System.out.println(s);

StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。

如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了:

String[] arr = {"he", "llo", "world"}; 
StringBuilder s = new StringBuilder(); 
for (String value : arr) { 
    s.append(value); 
} 
System.out.println(s);

不过,使用 “+” 进行字符串拼接会产生大量的临时对象的问题在 JDK9 中得到了解决。在 JDK9 当中,字符串相加 “+” 改为了用动态方法 makeConcatWithConstants() 来实现,而不是大量的 StringBuilder 了。这个改进是 JDK9 的 JEP 280open in new window 提出的,这也意味着 JDK 9 之后,你可以放心使用“+” 进行字符串拼接了。

String#equals() 和 Object#equals() 有何区别?

字符串常量池的作用了解吗?

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“ 
// 将字符串对象”ab“的引用保存在字符串常量池中 
String aa = "ab"; 
// 直接返回字符串常量池中字符串对象”ab“的引用 
String bb = "ab"; 
System.out.println(aa==bb); // true

String s1 = new String("abc");这句话创建了几个字符串对象?

1或2个

intern方法有什么作用?

String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:

  • 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
  • 如果字符串常量池中没有保存对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。

image.png

String类型的变量和常量做"+"运算时发生了什么?

先来看字符串不加final关键字拼接的情况(JDK 1.8):

String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing"; // 常量池中的对象
String str4 = str1 + str2; // 在堆上创建的新的对象
String str5 = "string"; // 常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

对于编译期可以确定值的字符串,也就是常量字符串,jvm会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放在字符串常量池,这个得益于编译器的优化。

在编译过程中,javac编译器会进行一个叫做 常量折叠(Constant Folding)的代码优化。

常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这厮javac编译器会对源代码做的极少量优化措施之一(因为代码优化几乎都在即时编译器JIT中进行)。

对于 String str3 = "str" + "ing"; ,javac编译器会给你优化成String str3 = "string";

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

  • 基本数据类型( bytebooleanshortcharintfloatlongdouble)以及字符串常量。

  • final 修饰的基本数据类型和字符串变量

  • 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

引用的值在程序编译器是无法确定的,编译器无法对其进行优化。

对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

String str4 = new StringBuilder().append(str1).append(str2).toString();

我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer

不过,字符串使用final关键字声明之后,可以让编译器当作常量来处理

示例代码:

final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing"; // 常量池中的对象 会发生常量折叠
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d); // true

final关键字修饰之后的String会被编译器当做常量来处理,编译器在程序编译器就可以确定它的值,其效果就相当于访问常量。

如果,编译器在运行时才能知道其确切值的话,就无法对其优化。

示例代码:

final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
    return "ing";
}