我将从Java基础、集合框架、多线程并发、JVM等方面,结合实际应用场景,为你总结校招Java面试常见知识点。
校招Java面试常见知识点全解析
对于准备校招进入Java开发领域的同学来说,掌握常见的面试知识点至关重要。本文将全面梳理校招Java面试中高频出现的知识点,并结合实际应用场景进行讲解,助力同学们顺利通过面试。
Java基础
面向对象特性
Java是一门面向对象的编程语言,具备封装、继承和多态三大特性。
封装:将数据和操作数据的方法封装在一起,隐藏内部实现细节,只对外提供必要的接口。例如,一个
BankAccount
类,将账户余额balance
设为私有属性,通过deposit
和withdraw
等公共方法来操作余额,保证了数据的安全性。public class BankAccount { private double balance; public void deposit(double amount) { if (amount > 0) { balance += amount; } } public void withdraw(double amount) { if (amount > 0 && amount <= balance) { balance -= amount; } } }
继承:子类可以继承父类的属性和方法,实现代码复用。比如,
Animal
类是父类,Dog
类继承自Animal
类,Dog
类就可以拥有Animal
类的一些通用属性和方法,如name
、age
以及move
方法等。
```java
class Animal {
private String name;
private int age;public Animal(String name, int age) {
this.name = name; this.age = age;
}
public void move() {
System.out.println("The animal is moving.");
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
}
- **多态**:同一接口,不同实现。可以通过父类引用指向子类对象,在运行时根据实际对象类型调用相应的方法。例如,`Animal`类有一个`makeSound`抽象方法,`Dog`类和`Cat`类分别重写该方法,当使用`Animal`类型的变量指向`Dog`或`Cat`对象时,调用`makeSound`方法会产生不同的效果。
```java
abstract class Animal {
public abstract void makeSound();
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.makeSound(); // 输出 Woof!
cat.makeSound(); // 输出 Meow!
}
}
数据类型与包装类
Java有8种基本数据类型,如byte
、short
、int
、long
、float
、double
、char
、boolean
。它们和对应的包装类(Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
)可以自动拆装箱。自动装箱是指基本数据类型自动转换为包装类,自动拆箱则相反。例如:
// 自动装箱
Integer num1 = 10;
// 自动拆箱
int num2 = num1;
基本数据类型存储在栈中,而包装类是对象,存储在堆中。包装类提供了一些实用的方法,如Integer.parseInt
可以将字符串转换为整数。
字符串相关
- String类:
String
类代表字符串,其对象一旦创建就不可变。这使得String
在多线程环境下天然安全,并且适合作为HashMap
的键,因为其哈希值可以缓存。例如:String s1 = "Hello"; String s2 = "Hello"; System.out.println(s1 == s2); // 输出 true,因为字符串常量池的存在,s1和s2指向同一个对象
- StringBuffer和StringBuilder:与
String
不同,StringBuffer
和StringBuilder
的对象是可变的。StringBuffer
是线程安全的,方法都用synchronized
修饰;StringBuilder
是非线程安全的,但效率更高。在需要频繁修改字符串的场景下,如构建SQL语句,应优先使用StringBuilder
。StringBuilder sb = new StringBuilder(); sb.append("SELECT * FROM users WHERE age > "); sb.append(18); String sql = sb.toString();
关键字
- final:修饰类时,该类不能被继承,如
String
类;修饰方法时,该方法不能被子类重写;修饰变量时,基本类型变量值不可变,引用类型变量地址不可变。
```java
final class FinalClass {} // 该类不能被继承
class Parent {
public final void finalMethod() {} // 该方法不能被子类重写
}
class Child extends Parent {
// 编译错误,不能重写final方法
// @Override
// public void finalMethod() {}
}
final int num = 10; // num的值不能改变
final List list = new ArrayList<>();
// list.add(1); 可以添加元素,因为list的地址不能变,但内容可变
- **static**:修饰成员变量时,该变量为类变量,所有对象共享;修饰成员方法时,该方法为类方法,可以通过类名直接调用,无需创建对象;修饰代码块时,为静态代码块,在类加载时执行,且只执行一次。
```java
public class StaticExample {
static int count = 0;
public StaticExample() {
count++;
}
public static void printCount() {
System.out.println("Count: " + count);
}
static {
System.out.println("Static block executed.");
}
}
public class Main {
public static void main(String[] args) {
StaticExample.printCount(); // 输出 Count: 0
StaticExample ex1 = new StaticExample();
StaticExample ex2 = new StaticExample();
StaticExample.printCount(); // 输出 Count: 2
}
}
异常处理
Java通过try - catch - finally
块来处理异常。try
块中放置可能会抛出异常的代码,catch
块捕获并处理异常,finally
块无论是否发生异常都会执行。例如:
try {
int result = 10 / 0; // 会抛出ArithmeticException异常
} catch (ArithmeticException e) {
System.out.println("Caught ArithmeticException: " + e.getMessage());
} finally {
System.out.println("Finally block executed.");
}
异常分为受检异常(如IOException
)和非受检异常(如RuntimeException
及其子类)。受检异常必须在方法声明中使用throws
声明或者在方法内捕获处理;非受检异常可以不做显式处理,但可能导致程序崩溃。
集合框架
List接口
- ArrayList:基于数组实现,支持随机访问,查询效率高(时间复杂度为O(1)),但在插入和删除元素时,尤其是在中间位置,需要移动大量元素,效率较低(时间复杂度为O(n))。例如:
List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); int num = list.get(1); // 高效获取元素,时间复杂度O(1) list.add(1, 10); // 在索引1处插入元素,后续元素需移动,时间复杂度O(n)
- LinkedList:基于链表实现,插入和删除元素效率高(时间复杂度为O(1)),因为只需修改相邻节点的引用,但随机访问效率低(时间复杂度为O(n)),需要从头遍历链表。例如:
List<Integer> linkedList = new LinkedList<>(); linkedList.add(1); linkedList.add(2); linkedList.addFirst(10); // 在头部插入元素,效率高,时间复杂度O(1) int num = linkedList.get(1); // 访问元素需遍历链表,时间复杂度O(n)
Set接口
- HashSet:基于
HashMap
实现,不允许重复元素,元素无序。它通过哈希值来确定元素的存储位置,因此插入和查询效率较高(平均时间复杂度为O(1))。例如:Set<String> set = new HashSet<>(); set.add("apple"); set.add("banana"); set.add("apple"); // 重复元素,不会被添加 boolean contains = set.contains("banana"); // 查询元素,效率高
- TreeSet:基于红黑树实现,元素有序(默认按自然顺序排序,也可自定义排序规则),不允许重复元素。插入和查询的时间复杂度为O(log n)。例如:
Set<Integer> treeSet = new TreeSet<>(); treeSet.add(3); treeSet.add(1); treeSet.add(2); for (int num : treeSet) { System.out.println(num); // 输出 1, 2, 3,有序 }
Map接口
- HashMap:JDK8之前底层是数组 + 链表结构,JDK8及之后在链表长度大于8时会转换为红黑树结构以提高查询效率。它允许
null
作为键和值,非线程安全。例如:Map<String, Integer> map = new HashMap<>(); map.put("apple", 1); map.put("banana", 2); map.put(null, 3); // 允许null键 Integer value = map.get("apple");
- ConcurrentHashMap:线程安全的
Map
实现。JDK7采用分段锁机制,将数据分成多个段,不同段可以并发操作;JDK8采用CAS + synchronized锁单个Node的方式实现线程安全,性能更好。例如:ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("apple", 1); concurrentMap.put("banana", 2); Integer value = concurrentMap.get("apple");
- Hashtable:线程安全,方法使用
synchronized
修饰,但性能较低。不允许null
作为键和值。例如:Hashtable<String, Integer> hashtable = new Hashtable<>(); hashtable.put("apple", 1); // hashtable.put(null, 2); // 抛出NullPointerException
多线程并发
线程创建与启动
- 继承Thread类:创建一个类继承
Thread
类,重写run
方法,然后创建该类的对象并调用start
方法启动线程。例如:class MyThread extends Thread { @Override public void run() { System.out.println("Thread is running."); } }
public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } }
- 实现Runnable接口:创建一个类实现
Runnable
接口,实现run
方法,将该类的对象作为参数传递给Thread
类的构造函数,再调用start
方法启动线程。这种方式更灵活,因为Java不支持多继承,一个类实现Runnable
接口后还可以继承其他类。例如:class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable is running."); } }
public class Main { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); } }
- 使用Callable接口:
Callable
接口与Runnable
类似,但Callable
的call
方法有返回值,并且可以抛出异常。通常结合FutureTask
类使用,通过FutureTask
获取线程执行结果。例如:
```java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
return 1 + 2;
}
}
```java
public class Main {
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
Integer result = futureTask.get(); // 获取线程执行结果
System.out.println("Result: " + result);
}
}
线程同步
synchronized关键字:可以修饰方法或代码块。修饰实例方法时,锁住的是当前对象;修饰静态方法时,锁住的是当前类的
Class
对象;修饰代码块时,可以指定锁对象。例如:public class SynchronizedExample { private int count = 0; public synchronized void increment() { count++; } public void incrementBlock() { synchronized (this) { count++; } } }
- ReentrantLock:与
synchronized
类似,但功能更强大,如可中断的锁获取、公平锁与非公平锁、锁绑定多个条件等。例如:
```java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
### 线程池
线程池可以复用线程,减少线程创建和销毁的开销,提高系统性能。常见的线程池创建方式是使用`ThreadPoolExecutor`类,它有七大参数:
```java
ThreadPoolExecutor(
int corePoolSize, // 核心线程数,线程池长期维持的线程数
int maximumPoolSize, // 最大线程数,线程池允许创建的最大线程数
long keepAliveTime, // 空闲线程存活时间,超过这个时间,空闲线程将被销毁
TimeUnit unit, // 时间单位,如TimeUnit.SECONDS
BlockingQueue<Runnable> workQueue, // 任务队列,用于存放等待执行的任务
ThreadFactory threadFactory, // 线程工厂,用于创建线程
RejectedExecutionHandler handler // 拒绝策略,当任务队列满且线程数达到最大线程数时,如何处理新任务
)
例如,创建一个固定大小的线程池:
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
int taskNum = i;
executorService.submit(() -> {
System.out.println("Task " + taskNum + " is running.");
});
}
executorService.shutdown();
JVM
内存区域
JVM内存主要分为以下几个区域:
- 程序计数器:线程私有,记录当前线程执行的字节码指令地址。
- 虚拟机栈:线程私有,每个方法在执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:与虚拟机栈类似,不过它为本地方法服务。
- 堆:线程共享,是Java对象分配的主要区域,所有的对象实例和数组都在这里分配内存。堆又可以分为新生代和老年代,新生代包括一个Eden区和两个Survivor区。
- 方法区:线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JDK8及之后,方法区被元空间(Meta Space)取代,元空间使用本地内存。
垃圾回收机制
垃圾回收(GC)主要负责回收堆内存中不再被引用的对象,释放内存空间。常见的垃圾回收算法有:
- 标记 - 清除算法:先标记所有可达对象,然后清除未被标记的对象。缺点是会产生大量不连续的内存碎片。
- 复制算法:将内存分为两块,每次只使用其中一块,当这块内存满时,将存活对象复制到另一块内存,然后清除原来的内存。适用于新生代,因为新生代对象存活率低。
- 标记 - 整理算法:先标记可达对象,然后将存活对象向一端移动,最后清除边界以外的内存。适用于老年代,因为老年代对象存活率高。
- 分代收集算法:根据对象存活周期的不同将内存划分为不同的区域,对不同区域采用不同的垃圾回收算法。新生代采用复制算法,老年代采用标记 - 整理算法。
类加载机制
类加载过程分为加载、验证、准备、解析和初始化五个阶段:
- 加载:通过类的全限定名获取二进制字节流,将其加载到内存中,生成一个代表该类的`Class
Java 基础,面向对象编程,集合框架,多线程,并发编程,Java 虚拟机(JVM),Spring 框架,MyBatis, 数据库(MySQL), 数据结构与算法,网络编程,设计模式,RESTful API, 微服务,校招面试技巧
资源地址:
https://pan.quark.cn/s/14fcf913bae6