Java 基础面试一
一、引用数据类型与基本数据类型的差异
引用数据类型与基本数据类型的核心区别在于存储内容和内存占用。引用数据类型不仅存储数据,还封装了操作数据的方法,如类和对象大多基于此类型构建。但相比基本数据类型,它因额外的对象头信息而占用更多内存空间。
在比较操作中,基本数据类型用 ==
比较值,引用数据类型用 ==
比较内存地址。例如:
int a = 10;
int b = 10;
System.out.println(a == b); // true,比较值相等
Integer c = new Integer(10);
Integer d = new Integer(10);
System.out.println(c == d); // false,比较地址不同
二、Integer 赋值 100 与 1000 的区别
当使用 Integer x = 100;
和 Integer y = 100;
时,x == y
返回 true
。这是因为 Java 为常用的小整数(-128 到 127)设立了缓存池,相同值直接引用池中对象。而超出此范围的值(如 1000),每次赋值都新建对象,故 Integer x = 1000; Integer y = 1000;
时,x == y
返回 false
。但 int
与 Integer
比较时会自动拆箱为基本类型,比较的是值。
Integer x = 100;
Integer y = 100;
System.out.println(x == y); // true,引用池中同一对象
Integer x = 1000;
Integer y = 1000;
System.out.println(x == y); // false,新建不同对象
int x = 1000;
Integer y = 1000;
System.out.println(x == y); // true,自动拆箱后比较值
三、类对象的加载过程
类的加载始于类被加载至方法区。之后,JVM 在主线程栈中执行 main
方法,进行局部变量赋值等操作。遇到 new
关键字时,在堆内存创建对象,初始化对象字段,完成对象的构建。
public class Example {
public static void main(String[] args) {
MyClass obj = new MyClass(); // 加载类、执行 main 方法、创建对象
}
}
class MyClass {
int value;
}
四、static 与 abstract 的冲突
static
和 abstract
不能同时修饰同一个成员。static
表明成员属于类本身,而 abstract
要求子类实现抽象方法。二者目的相悖,故不能共存。
// 错误示例
public abstract class MyClass {
public static abstract void myMethod(); // 编译错误
}
五、Java 三大特性详解
- 继承 :子类继承父类方法和变量,实现代码复用。
- 封装 :将数据和操作封装,提高代码安全性和可维护性,如类通过私有成员隐藏实现细节。
- 多态 :父类引用可指向子类对象,调用方法时执行子类特定实现,增强代码灵活性。
// 多态示例
class Animal {
void makeSound() {}
}
class Dog extends Animal {
void makeSound() { System.out.println("汪汪"); }
}
class Cat extends Animal {
void makeSound() { System.out.println("喵喵"); }
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.makeSound(); // 输出 "汪汪"
cat.makeSound(); // 输出 "喵喵"
}
}
六、静态代码块与实例代码块的区别
静态代码块在类加载时执行,仅执行一次,常用于类的初始化操作。实例代码块在每次创建对象时执行,用于对象的初始化。
class MyClass {
static { System.out.println("静态代码块执行"); } // 类加载时执行
{ System.out.println("实例代码块执行"); } // 对象创建时执行
}
public class CodeBlockExample {
public static void main(String[] args) {
new MyClass(); // 输出:静态代码块执行、实例代码块执行
new MyClass(); // 输出:实例代码块执行
}
}
七、==
与 equals
的区别
==
对基本数据类型比较值,引用数据类型比较地址。equals
用于比较对象内容,可重写以实现自定义比较逻辑。
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false,比较地址不同
System.out.println(s1.equals(s2)); // true,内容相同
八、JDK 1.8 前后字符串常量池的变化
JDK 1.8 前,字符串常量池在永久代,空间有限,易溢出,且字符串引用和创建可能在堆和常量池间复制。JDK 1.8 后,字符串常量池移至堆中,优化了存储和引用效率。
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true,JDK 1.8 及之后指向堆中同一地址
九、String、StringBuffer、StringBuilder 的差异
- String :不可变,每次修改都创建新对象,线程安全。
- StringBuffer :可变,线程安全但效率低,因采用
synchronized
。 - StringBuilder :可变,效率高但线程不安全,适合单线程环境。
String s = "hello";
s += " world"; // 实际创建新字符串
StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); // 直接修改对象
十、ArrayList 的扩容机制
ArrayList
初始容量为 0,第一次扩容至 10,之后每次扩容为原容量的 1.5 倍。扩容时会创建新数组并复制数据,影响性能,故建议提前预估容量。
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 15; i++) {
list.add(i);
if (i == 10) {
System.out.println("扩容后容量:" + ((ArrayList<?>) list).capacity()); // 输出扩容后的容量
}
}
十一、Vector 与 ArrayList 的区别
- 线程安全 :Vector 线程安全,ArrayList 不安全。
- 效率 :Vector 因线程安全机制效率低。
- 扩容方式 :Vector 扩容为原容量的 2 倍,ArrayList 为 1.5 倍。
Vector<Integer> vector = new Vector<>();
vector.add(1); // 同步操作
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1); // 非同步操作
十二、数组转列表与列表转数组的区别
Arrays.asList
将数组转为固定大小的列表,元素修改影响原数组。toArray
将集合转为数组,修改新数组不影响原集合。
Integer[] array = {1, 2, 3};
List<Integer> list = Arrays.asList(array); // 数组转列表
list.set(0, 10); // array[0] 变为 10
ArrayList<Integer> arrayList = new ArrayList<>(Arrays.asList(1, 2, 3));
Integer[] newArray = arrayList.toArray(new Integer[0]); // 列表转数组
newArray[0] = 10; // arrayList 中元素仍为 1
十三、ArrayList 与 LinkedList 的区别
- 底层结构 :ArrayList 基于动态数组,LinkedList 基于双向链表。
- 查询效率 :ArrayList 随机访问快,LinkedList 慢。
- 增删效率 :ArrayList 增删慢(需移动元素),LinkedList 快(只需修改指针)。
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(0, 1); // 可能需移动大量元素
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.addFirst(1); // 直接修改指针,无需移动元素
十四、Set 与 List 的区别
- 去重 :Set 自动去重,List 允许重复。
- 顺序 :Set 无序,List 有序。
- 操作效率 :Set 的增删查(基于哈希表实现)效率高,List 的查询快(基于索引),增删慢(需移动元素)。
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(1); // 重复添加无效
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(1); // 允许重复添加
十五、HashSet 的去重原理
HashSet
底层基于 HashMap
,元素作为键,固定值作为值。通过 hashCode
获取哈希值,结合 equals
比较元素内容,相同则视为重复并去重。
HashSet<String> set = new HashSet<>();
set.add("hello");
set.add("hello"); // 第二次添加返回 false,去重成功
十六、TreeSet 的排序与自定义对象排序
TreeSet
默认按元素自然顺序(通过 compareTo
方法)排序。自定义对象排序需实现 Comparable
接口定义自然顺序,或传入 Comparator
指定排序方式。
// 自定义对象实现 Comparable
class Person implements Comparable<Person> {
int age;
public Person(int age) {
this.age = age;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age); // 按年龄升序排序
}
}
TreeSet<Person> set = new TreeSet<>();
set.add(new Person(20));
set.add(new Person(18)); // 按 age 排序插入