基础概念
JDK、JRE、JVM 的区别?
- JDK 是 Java 开发工具包,包含了 Java 的 开发工具(编译工具
javac.exe
和打包工具jar.exe
等)和 JRE。 - JRE 是 Java 运行环境,提供了库、JVM 和其他组件,用于运行 Java 程序。
- JVM 负责把 Java 程序生成的字节码解释成具体系统平台上的机器指令,让其在各个平台上运行。
JDK = 开发工具 + JRE,JRE = JVM + 类库。
final
关键字的作用是什么:
final
修饰的类不能被继承final
修饰的方法不能被重写final
修饰的变量必须被初始化,且不能被修改
包装类型的缓存机制是什么?
Integer 缓存池的大小默认为 -128 ~ 127
,调用 Integer.valueOf(intNum)
方法时,如果 intNum
对象存在于缓存池中,会直接使用这个对象,而不是新建对象。
Integer x = new Integer(123); // 创建新对象
Integer y = new Integer(123); // 创建新对象
x == y; // false
Integer a = Integer.valueOf(123); // 使用缓存池中的对象
Integer b = Integer.valueOf(123); // 使用缓存池中的对象
a == b; // true
自动装箱时,如果数值位于缓冲池范围内,会隐式调用 valueOf
方法。
Byte
、Short
、Integer
、Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据。而Double
和Float
没有实现缓存机制。
访问权限修饰符有哪些?
- private:当前类内部可见。
- protected:用于 修饰成员,当前 package 和所有子类可见。
- default:当前 package 可见。
- public:所有类可见。
初始化顺序是什么?
- 父类的静态变量、静态语句块
- 当前类的静态变量、静态语句块
- 父类的实例变量、普通语句块
- 父类的构造函数
- 当前类的实例变量、普通语句块
- 当前类的构造函数
静态成员是属于类的,在类加载时就会分配内存。而非静态成员属于实例对象,只有在对象实例化之后才存在。
Exception
有哪些种类?
- RuntimeException:运行时异常(非受查异常),在编译时不会被发现。这些异常一般 是由程序逻辑错误引起的,应该由程序员来避免。
NullPointerException
、IndexOutOfBoundsException
、IllegalArgumentException
…… - 编译时异常:(受查异常),如果不处理这种类型的异常,程序就不能编译通过。
IOException
:FileNotFoundException
SQLException
面向对象
重写和重载的方法是什么?
- 重写(Override):指 子类 实现了一个与父类的方法签名相同的方法。
- 重载(Overload):在 同一个类中,一个方法与另一个已存在方法的 方法名相同,但是 参数类型、个数、顺序至少有一个不同 。
只有返回类型不同不是重载。
==
和 equals
的区别是什么?
==
对基本类型比较值,对对象类型比较引用(地址值)。equals
默认情况下是比较引用,但是很多类重写了equals
方法,比如String
、Integer
类,将它变成了 值的比较,所以一般情况下是比较值是否相等。
抽象类和接口的区别是什么?
语法层面:
- 抽象类的 成员变量 可以是各种类型的,接口的只能是
public static final
。 - 抽象类可以有
static
代码块和static
方法,接口没有。 - 一个类只能继承一个抽象类,但可以实现多个接口。
设计层面:
- 抽象类是对 一类事物 的抽象,接口是对 行为 的抽象。
- 抽象类作为 父类,是一种 模板式 设计,而接口作为 行为规范,是一种 辐射式 设计。
在多数情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变得很低。
深拷贝、浅拷贝、引用拷贝的区别是什么?
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
hashCode()
和 equals()
的关系是什么?
hashCode()
的作用是获取哈希码,作用是确定该对象在哈希表中的索引位置。以 HashSet
检查重复 为例,加入一个对象时,会先调用 hashCode()
来计算该对象应该插入的位置,如果该位置处没有其他对象,说明当前对象不重复。而如果当前位置处存在其他对象,这时候 不能说明重复,需要 进一步调用 equals()
方法来判断 当前对象和已存在对象是否真的重复。也就是说,只有在 hashCode
相同时才会调用 equals()
方法,大大降低了开销。
总结来说:
- 如果两个对象的
hashCode
不相等,则这两个对象 不相等。 - 如果两个对象的
hashCode
相等,那么这两个对象 不一定相等(哈希碰撞)。需要同时满足equals()
条件,才能说明相等。 - 如果
equals()
返回true
,那么说明两个对象相等,则它们的hashCode
必定相等。
常用类
String 为什么不可变?
String
内部使用 char[]
数组存储数据,该数组被声明为 final
,这意味着 value
数组初始化之后就不能引用其他数组;并且 String
内部没有改变 value
数组的方法,因此可以保证 String
不可变。
intern()
方法的作用是什么?
str.intern()
会先检查 字符串常量池 中是否有 str
这个字符串,如果存在则 直接返回这个字符串的引用,否则将这个字符串 添加到字符串常量池中,然后再返回这个字符串的引用。
使用构造函数创建的字符串 new String("xxx")
不会添加到字符串常量池中,使用字面量 "xxx"
创建的字符串才会添加到字符串常量池中(如果常量池中已经存在该字符串,则直接返回这个引用)。
String str1 = "Rachael"; // 添加到常量池
String str2 = new String("Rachael");
str1 == str2; // false
str2.intern() == str1; // true
另外,使用多个字符串字面量拼接来创建字符串时,会将其拼接结果作为字面量来创建字符串。
String s1 = "abc";
String s2 = "ab" + "c";
s1 == s2; // true
对于 编译期可以确定值 的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
字符串拼接用 +
还是 StringBuilder
?
字符串对象通过 +
拼接时,会创建 StringBuilder
对象,调用 append()
方法来实现,拼接完成之后调用 toString()
得到一个 String
对象 。
不过,在循环内使用 +
进行字符串的拼接的话,存在比较明显的缺陷:StringBuilder
对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder
对象。
容器
HashMap
和 HashTable
有什么区别?
- 继承关系:
HashMap
继承自抽象类AbstractMap
;HashTable
继承自Dictionary
抽象类,该类已被废弃。 - null 键和值:
HashMap
支持空键和空值,而HashTable
会抛出空指针异常。因为HashMap
将null
的hashCode
定为了0
,从而将它放在哈希表的第0
个桶中。 - 数据结构:都用哈希表来存储键值对,哈希表用
Entry
类型的引用数组来实现,数组长度就是哈希桶的数量。数组中 每个Entry
引用指向一个链表结点,这个结点又包含着下一个结点的引用。(使用拉链法解决散列冲突)JDK 8 开始,当HashMap
的某条链表长度大于 8 之后,会将链表转化为红黑树。(转化为红黑树前会先判断,如果当前数组长度小于 64,会进行数组扩容;否则才进行转化。) - 初始容量和扩容:
HashTable
的默认初始大小为11
,每次扩容会将原容量n
扩展为2n+1
;HashMap
的默认初始大小为16
,每次扩容会将容量 翻倍。
HashTable
已被淘汰,不要使用。
ConcurrentHashMap
和 HashTable
有什么区别?
主要是 实现线程安全的方式 不同。
-
底层数据结构:
- JDK 7 的
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK 8 采用的数据结构跟HashMap
的结构一样,数组+链表/红黑树。 Hashtable
和 JDK 8 之前的HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的。
- JDK 7 的
-
实现线程安全的方式:
-
在 JDK1.7 的时候,
ConcurrentHashMap
对整个桶数组进行了分段(segment),分段加锁,每一把锁只锁容器中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。Segment
的结构和HashMap
类似,是一种数组和链表结构,一个Segment
包含一个HashEntry
数组,每个HashEntry
是一个链表结构的元素。 -
到了 JDK1.8 的时候,
ConcurrentHashMap
已经摒弃了Segment
的概念,而是直接用Node
数组+链表/红黑树 的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。锁粒度更细,synchronized
只锁定当前链表或红黑树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。 -
Hashtable
使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态。
-
List
和 Set
的 contains()
方法有什么区别?
HashSet
的contains()
方法底部依赖的HashMap
的containsKey()
方法,时间复杂度接近于O(1)
(没有出现哈希冲突的时候为O(1)
)ArrayList
的contains()
方法是通过遍历所有元素的方法来做的,时间复杂度接近是O(n)
。
将数组转换为 List 的方法有哪些?
-
List list = new ArrayList<>(Arrays.asList(arr));
-
使用 Stream
// 包装类型数组 List list = Arrays.stream(arr).collect(Collectors.toList()); // 基本类型数组 List list = Arrays.stream(arr).boxed().collect(Collectors.toList());
标签:面试题,01,Java,String,对象,链表,数组,字符串,Integer From: https://www.cnblogs.com/lzh1995/p/16758004.html
Arrays.asList()
方法返回的不是java.util.List
类型,而是一个 Arrays 的子类型。