java面试题
疫情结束后,又一轮找工作,准备了一波面试题,希望可以帮到大家
1.String类面试题
1).String, Stringbuffer, StringBuilder 的区别。
String 字符串常量(final修饰,不可被继承),String是常量,当创建之后即不能更改。(可以通过StringBuffer和StringBuilder创建String对象(常用的两个字符串操作类)。)
StringBuffer 字符串变量(线程安全),其也是final类别的,不允许被继承,其中的绝大多数方法都进行了同步处理,包括常用的Append方法也做了同步处理(synchronized修饰)。其自jdk1.0起就已经出现。其toString方法会进行对象缓存,以减少元素复制开销
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
StringBuilder 字符串变量(非线程安全)。与StringBuffer一样都继承和实现了同样的接口和类,方法除了没使用synch修饰以外基本一致,不同之处在于最后toString的时候,会直接返回一个新对象。
public String toString() {
return new String(value, 0, count);
}
2).字符串拼接问题
public class TestString {
private static String str = "awe";
public static void main(String[] args) {
String str1 = "qw";
String str2 = "e";
String combo = str1 + str2;
System.out.println(str == combo);
System.out.println(str == combo.intern());
}
}
打印结果
false
true
这个例子用来说明用+连接字符串时,实际上是在堆内容创建对象,那么combo指向的是堆内存存储”abc”字符串的空间首地址,显然str==combo是错误的,而str==combo.intern()是正确的,在String池中也存在”abc”,那就直接返回了,而str也是指向String池中的”abc”对象的。此例说明任何重新修改String都是重新分配内存空间,这就使得String对象之间互不干扰。也就是String中的内容一旦生成不可改变,直至生成新的对象
同时问题也来了,使用+连接字符串每次都生成新的对象,而且是在堆内存上进行,而堆内存速度比较慢(相对而言),那么再大量连接字符串时直接+是不可取的,当然需要一种效率高的方法。Java提供的StringBuffer和StringBuilder就是解决这个问题的。区别是前者是线程安全的而后者是非线程安全的,StringBuilder在JDK1.5之后才有。不保证安全的StringBuilder有比StringBuffer更高的效率。
3).关于String.intern()方法
String.intern()方法是一种手动将字符串加入常量池中的方法,原理如下:如果在常量池中存在与调用intern()方法的字符串等值的字符串,就直接返回常量池中相应字符串的引用,否则在常量池中复制一份该字符串,并将其引用返回(Jdk7中会直接在常量池中保存当前字符串的引用);Jdk6 中常量池位于PremGen区,大小受限,不建议使用String.intern()方法,不过Jdk7 将常量池移到了Java堆区,大小可控,可以重新考虑使用String.intern()方法,但是由对比测试可知,使用该方法的耗时不容忽视,所以需要慎重考虑该方法的使用;String.intern()方法主要适用于程序中需要保存有限个会被反复使用的值的场景,这样可以减少内存消耗,同时在进行比较操作时减少时耗,提高程序性能。
String.intern()的使用场景
public class InternTest {
public static void main(String[] args) {
print("noIntern: " + noIntern());
print("intern: " + intern());
}
private static long noIntern(){
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
int j = i % 50;
String str = String.valueOf(j);
}
return System.currentTimeMillis() - start;
}
private static long intern(){
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
int j = i % 50;
String str = String.valueOf(j).intern();
}
return System.currentTimeMillis() - start;
}
}
打印结果
noIntern: 76
intern: 149
从输出结果可以看出,若是单纯使用intern()方法进行数据存储的话,程序运行时间要远高于未使用intern()方法时。
由于intern()操作每次都需要与常量池中的数据进行比较以查看常量池中是否存在等值数据,同时JVM需要确保常量池中的数据的唯一性,这就涉及到加锁机制,这些操作都是有需要占用CPU时间的,所以如果进行intern操作的是大量不会被重复利用的String的话,则有点得不偿失。由此可见,String.intern()主要 适用于只有有限值,并且这些有限值会被重复利用的场景。
4).String能被继承吗
不可以,因为String类有final修饰符,而final修饰的类是不能被继承的,实现细节不允许改变
2.ArrayList 和 LinkedList 相关
1).ArrayList
List中主要有ArrayList、LinkedList两个实现类;Set中则是有HashSet实现类;而Queue是在JDK1.5后才出现的新集合,主要以数组和链表两种形式存在。
在Collection中,List集合是有序的,可对其中每个元素的插入位置进行精确地控制,可以通过索引来访问元素,遍历元素。
它继承于AbstractList,实现了List, RandomAccess, Cloneable, Serializable接口。
1.ArrayList实现List,得到了List集合框架基础功能;
2.ArrayList实现RandomAccess,获得了快速随机访问存储元素的功能,RandomAccess是一个标记接口,没有任何方法;
3.ArrayList实现Cloneable,得到了clone()方法,可以实现克隆功能;
4.ArrayList实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议。
它具有如下特点:
容量不固定,随着容量的增加而动态扩容(阈值基本不会达到)
有序集合(插入的顺序==输出的顺序)
插入的元素可以为null
增删改查效率更高(相对于LinkedList来说)
线程不安全
2).LinkedList
LinkedList是一个双向链表,每一个节点都拥有指向前后节点的引用。相比于ArrayList来说,LinkedList的随机访问效率更低。
它继承AbstractSequentialList,实现了List, Deque, Cloneable, Serializable接口。
1.LinkedList实现List,得到了List集合框架基础功能;
2.LinkedList实现Deque,Deque 是一个双向队列,也就是既可以先入先出,又可以先入后出,说简单些就是既可以在头部添加元素,也可以在尾部添加元素;
3.LinkedList实现Cloneable,得到了clone()方法,可以实现克隆功能;
4.LinkedList实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议。
3). ArrayList和LinkedList比较
元素新增性能比较
ArrayList效率不如LinkedList,因为ArrayList底层是数组实现,在动态扩容时,性能有所损耗,而LinkedList不存在数组扩容机制,所以LinkedList效率更高。
但是JDK近几年的更新发展,对于数组复制的实现进行了优化,以至于ArrayList的性能也得到了提高。
4).元素获取比较
由于LinkedList是链表结构,没有角标的概念,没有实现RandomAccess接口,
不具备随机元素访问功能,所以在随机访问方面,ArrayList完胜。
LinkedList为什么这么慢呢?这主要是LinkedList的代码实现所致,每一次获取
都是从头开始遍历,一个个节点去查找,每查找一次就遍历一次,所以性能自然得不到提升。
5).ArrayList和LinkedList的扩容
ArrayList初始大小为10,每次1.5倍进行扩容。
LinkedList是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。
3.类加载相关
1).类加载
类加载分为三个步骤:加载、连接、初始化。
1.加载
类加载指的是将class文件读入内存,并为之创建一个java.lang.Class对象,即程序中使用任何类时,系统都会为之建立一个java.lang.Class对象,系统中所有的类都是java.lang.Class的实例。
类的加载由类加载器完成,JVM提供的类加载器叫做系统类加载器,此外还可以通过继承ClassLoader基类来自定义类加载器。
通常可以用如下几种方式加载类的二进制数据:
从本地文件系统加载class文件。
从JAR包中加载class文件,如JAR包的数据库启驱动类。
通过网络加载class文件。
把一个Java源文件动态编译并执行加载。
2.连接
连接阶段负责把类的二进制数据合并到JRE中,其又可分为如下三个阶段:
验证:确保加载的类信息符合JVM规范,无安全方面的问题。
准备:为类的静态Field分配内存,并设置初始值。
解析:将类的二进制数据中的符号引用替换成直接引用。
3.初始化
该阶段主要是对静态Field进行初始化,在Java类中对静态Field指定初始值有两种方式:
声明时即指定初始值,如static int c = 7;
使用静态代码块为静态Field指定初始值,如:static{ w = 3; }
JVM初始化一个类包含如下几个步骤:
假如这个类还没有被加载和连接,则程序先加载并连接该类。
假如该类的直接父类还没有被初始化,则先初始化其直接父类。
假如类中有初始化语句,则系统依次执行这些初始化语句。
所以JVM总是最先初始化java.lang.Object类。
类初始化的时机(对类进行主动引用时):
创建类的实例时(new、反射、反序列化)。
调用某个类的静态方法时。
使用某个类或接口的静态Field或对该Field赋值时。
使用反射来强制创建某个类或接口对应的java.lang.Class对象,如Class.forName("Demo")
初始化某个类的子类时,此时该子类的所有父类都会被初始化。
直接使用java.exe运行某个主类时。
2).类的实例化顺序
父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当 new 的时候, 他们的执行顺序。
类加载器实例化时进行的操作步骤(加载–>连接->初始化)。
父类静态代变量、
父类静态代码块、
子类静态变量、
子类静态代码块、
父类非静态变量(父类实例成员变量)、
父类构造函数、
子类非静态变量(子类实例成员变量)、
子类构造函数。
3).类加载器
当JVM启动时,会形成有3个类加载器组成的初始类加载器层次结构:
1.Bootstrap ClassLoader:根类(或叫启动、引导类加载器)加载器。
它负责加载Java的核心类(如String、System等)。它比较特殊,因为它是由
原生C++代码实现的,并不是java.lang.ClassLoader的子类,所以下面的运行结果为null。
2.Extension ClassLoader:扩展类加载器。
它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext)中JAR包的类,我们
可以通过把自己开发的类打包成JAR文件放入扩展目录来为Java扩展核心类以外的新功能。
3.System ClassLoader(或Application ClassLoader):系统类加载器。
它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过
ClassLoader的静态方法getSystemClassLoader来获取系统类加载器。
4).类加载机制
JVM的类加载机制主要有以下3种:
全盘负责:当一个类加载器加载某个Class时,该Class所依赖和引用的其它
Class也将由该类加载器负责载入,除非显式的使用另外一个类加载器来载入。
双亲委派:当一个类加载器收到了类加载请求,它会把这个请求委派给父
(parent)类加载器去完成,依次递归,因此所有的加载请求最终都被传送到
顶层的启动类加载器中。只有在父类加载器无法加载该类时子类才尝试从自己
类的路径中加载该类。(注意:类加载器中的父子关系并不是类继承上的父子
关系,而是类加载器实例之间的关系。)
缓存机制:缓存机制会保证所有加载过的Class都会被缓存,当程序中需要使用
某个类时,类加载器先从缓冲区中搜寻该类,若搜寻不到将读取该类的二进制
数据,并转换成Class对象存入缓冲区中。这就是为什么修改了Class后需重启JVM才能生效的原因。
5).Java9类加载器的改变
JDK 9保持三级分层类加载器架构以实现向后兼容。但是,从模块系统加载类的
方式有一些变化。且新增Platform ClassLoader:平台类加载器,用于加载一些
平台相关的模块,例如: java.activation 、 java.se 、 jdk.desktop 、
java.compiler 等,双亲是BootClassLoader。
在JDK 9中,应用程序类加载器可以委托给平台类加载器以及引导类加载器;平
台类加载器可以委托给引导类加载器和应用程序类加载器。
此外,JDK 9不再支持扩展机制。 但是,它将扩展类加载器保留在名为平台类
加载器的新名称下。 ClassLoader类包含一个名为getPlatformClassLoader()的
静态方法,该方法返回对平台类加载器的引用。
在JDK 9之前,扩展类加载器和应用程序类加载器都是java.net.URLClassLoader
类的一个实例。 而在JDK 9中,平台类加载器(以前的扩展类加载器)和应用
程序类加载器是内部JDK类的实例。 如果你的代码依赖于URLClassLoader类的
特定方法,代码可能会在JDK 9中崩溃。
JDK 9中的类加载机制有所改变。 三个内置的类加载器一起协作来加载类。
当应用程序类加载器需要加载类时,它将搜索定义到所有类加载器的模
块。 如果有合适的模块定义在这些类加载器中,则该类加载器将加载类,这意
味着应用程序类加载器现在可以委托给引导类加载器和平台类加载器。 如果在
为这些类加载器定义的命名模块中找不到类,则应用程序类加载器将委托给其
父类,即平台类加载器。 如果类尚未加载,则应用程序类加载器将搜索类路
径。 如果它在类路径中找到类,它将作为其未命名模块的成员加载该类。 如果
在类路径中找不到类,则抛出ClassNotFoundException异常。
当平台类加载器需要加载类时,它将搜索定义到所有类加载器的模块。 如
果一个合适的模块被定义为这些类加载器中,则该类加载器加载该类。 这意味
着平台类加载器可以委托给引导类加载器以及应用程序类加载器。 如果在为这
些类加载器定义的命名模块中找不到一个类,那么平台类加载器将委托给它的
父类,即引导类加载器。
当引导类加载器需要加载一个类时,它会搜索自己的命名模块列表。 如果
找不到类,它将通过命令行选项-Xbootclasspath/a指定的文件和目录列表进行
搜索。 如果它在引导类路径上找到一个类,它将作为其未命名模块的成员加载
该类。
未完待续。。。。