Java 面试随着时间的改变而改变。在过去的日子里,当你知道 String 和 StringBuilder 的区别就能让你直接进入第二轮面试,但是现在问题变得越来越高级,面试官问的问题也更深入。
在我初入职场的时候,类似于 Vector 与 Array 的区别、HashMap 与 Hashtable 的区别是最流行的问题,只需要记住它们,就能在面试中获得更好的机会,但这种情形已经不复存在。
如今,你将会被问到许多 Java 程序员都没有看过的领域,如 NIO,[设计模式]“设计模式:可复用面向对象软件的基础”),成熟的单元测试,或者那些很难掌握的知识,如并发、算法、数据结构及编码。
Java 内存模型(JMM)是一种抽象的规范,定义了线程和主内存之间的抽象关系。线程之间的共享变量存储在主内存中,每个线程都有自己独立的工作内存,线程对变量的操作都在工作内存中进行,不能直接读写主内存中的变量。
重载体现了编译时多态性,在编译阶段,编译器会根据调用方法时传入的参数类型和个数来决定调用哪个方法。它增加了方法的灵活性,方便在不同场景下对相似功能进行统一命名。
重写体现了运行时多态性,在运行时,根据对象的实际类型来决定调用哪个类的重写方法。通过重写,子类可以根据自身特点定制父类方法的行为,实现不同子类对相同方法的不同实现,增强了代码的扩展性和可维护性。
泛型是 Java 5 引入的特性,它允许在定义类、接口和方法时使用类型参数,提高代码的复用性和安全性。但 Java 中的泛型在编译后会进行擦除,即编译后的字节码文件中,泛型类型信息会被擦除,替换为其限定类型(如果有泛型限定,如
ConcurrentHashMap 在高并发场景下具有出色的性能优势,其原理和与 HashTable 的对比如下:
TreeSet 基于红黑树(一种自平衡的二叉搜索树)实现,它能够对存储的元素进行排序。其实现原理和自定义排序方式如下:
然后创建 TreeSet 时,直接添加Student对象,TreeSet 会根据Student类中定义的compareTo方法进行排序。
线程池的工作原理涉及核心线程、最大线程、队列容量和拒绝策略之间的协同工作,具体如下:
在实际应用中,可以根据业务需求选择合适的拒绝策略,也可以自定义拒绝策略,实现RejectedExecutionHandler接口即可。例如,在一个订单处理系统中,如果订单提交任务过多,超过线程池处理能力,可以根据业务重要性,选择DiscardPolicy丢弃一些不重要的订单任务,或者使用CallerRunsPolicy让提交订单的线程自己处理订单,避免订单堆积导致系统崩溃。
AQS(AbstractQueuedSynchronizer)是 Java 并发包中实现锁和同步器的基础框架,它通过一个 FIFO 队列来管理等待获取同步状态的线程。
ArrayList 基于动态数组实现,它在内存中以连续的空间存储元素。正因如此,通过索引访问元素时速度极快,时间复杂度为 O (1),比如list.get(5)能迅速定位到第 6 个元素。但在进行插入和删除操作时,尤其是在列表中间位置,需要移动大量元素,性能较差,时间复杂度为 O (n) 。例如在 ArrayList 的头部插入一个元素,后续所有元素都要向后移动一位。它适用于需要频繁随机访问元素的场景,像数据统计分析中,经常需要快速获取特定位置的数据进行计算。
LinkedList 基于双向链表实现,每个节点都包含前驱节点和后继节点的引用。插入和删除操作只需要修改相关节点的引用即可,无需移动大量元素,在列表中间插入或删除元素的时间复杂度为 O (1) ,比如在链表中间某节点后插入新节点,只需调整几个引用关系。不过,由于它不是基于连续内存存储,无法通过索引直接访问元素,只能从头或尾开始遍历,访问元素的时间复杂度为 O (n) 。LinkedList 适用于需要频繁进行插入和删除操作的场景,例如在实现消息队列时,新消息不断插入队尾,处理后的消息从队头删除。
将数组转换为 List,可以使用 Arrays 类的asList方法。
需要注意的是,Arrays.asList返回的 List 是一个固定大小的 List,不支持添加或删除元素操作,如果尝试调用add或remove方法会抛出UnsupportedOperationException异常。如果需要一个可变的 List,可以再包装一层,使用 ArrayList 的构造函数:
String[] array = {"apple", "banana", "cherry"};
List
mutableList.add("date");
将 List 转换为数组,可以使用 List 的toArray方法。如果 List 中元素类型为 Object,可以直接调用toArray,它会返回一个 Object 数组:
List
Object[] array = list.toArray();
如果希望返回指定类型的数组,可以使用toArray(T[] a)方法,传入一个类型相同且长度合适的数组(若长度小于 List 大小,会创建一个新的合适长度的数组):
List
String[] array = list.toArray(new String[0]);
Iterator 是 Java 集合框架中用于遍历集合元素的迭代器,它可以用于遍历 List、Set 等实现了 Collection 接口的集合。Iterator 提供了hasNext方法用于判断是否还有下一个元素,next方法用于获取下一个元素,以及remove方法用于删除当前迭代到的元素。它只能单向遍历集合,从集合的开头向末尾移动,且不能在遍历过程中修改集合结构(除了使用remove方法),否则会抛出ConcurrentModificationException异常。
ListIterator 是 Iterator 的子接口,专门用于遍历 List 集合。它除了拥有 Iterator 的所有方法外,还增加了一些额外功能。它可以双向遍历 List,既可以使用next方法向前移动,也可以使用previous方法向后移动。并且提供了add方法用于在当前位置插入元素,set方法用于修改当前位置的元素,这使得在遍历 List 时可以更灵活地修改集合内容。例如,在遍历 List 时,发现某个元素不符合条件,可以使用 ListIterator 将其修改或在其前插入新元素。
进程是程序的一次执行过程,是系统进行资源分配和调度的一个独立单位。每个进程都有自己独立的内存空间、系统资源(如文件描述符、内存空间等),不同进程之间相互隔离,进程之间的通信需要借助特定的机制,如管道、消息队列、共享内存等。例如,当你同时打开浏览器和音乐播放器,它们就是两个不同的进程,各自独立运行,互不干扰,并且分别占用一定的系统资源。
线程是进程中的一个执行单元,是进程内的可调度实体。一个进程可以包含多个线程,同一进程内的线程共享进程的资源,如内存空间、文件描述符等,这使得线程间通信相对简单,例如通过共享变量就可以实现数据交换。但也正因如此,多线程编程需要注意线程安全问题,避免多个线程同时访问和修改共享资源导致数据不一致。线程的创建和销毁开销相对进程较小,在多任务处理场景下,使用多线程能更高效地利用 CPU 资源,提升程序的并发性能。例如,在一个网络服务器程序中,可以使用多线程来同时处理多个客户端的请求,每个线程负责与一个客户端进行通信和数据处理。
线程的生命周期有以下几种状态:
在 Java 中有两种常见的创建线程的方式:
第一种是继承 Thread 类。定义一个类继承自 Thread 类,重写其run方法,在run方法中编写线程执行的逻辑,然后创建该类的实例并调用start方法启动线程。示例代码如下:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("This is a thread created by extending Thread class.");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
第二种是实现 Runnable 接口。定义一个类实现 Runnable 接口,实现其run方法,然后创建一个 Thread 类的实例,并将实现了 Runnable 接口的类的实例作为参数传递给 Thread 类的构造函数,最后调用start方法启动线程。示例代码如下:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("This is a thread created by implementing Runnable interface.");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
相比继承 Thread 类,实现 Runnable 接口的方式更灵活,因为 Java 不支持多继承,一个类继承了 Thread 类就无法再继承其他类,而实现 Runnable 接口的类还可以继承其他类,并且多个线程可以共享同一个 Runnable 实例,更便于资源共享。在 Java 5 之后,还引入了 Callable 接口和 Future 接口来创建有返回值的线程,通过ExecutorService框架来管理和执行线程,这在需要获取线程执行结果的场景中非常有用。
线程同步的方式主要有以下几种:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public void incrementInBlock() {
synchronized (this) {
count++;
}
}
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,程序陷入僵持状态。例如,线程 A 持有资源 1 并等待获取资源 2,而线程 B 持有资源 2 并等待获取资源 1,此时两个线程互相等待对方释放资源,就形成了死锁。
死锁的产生需要同时满足四个必要条件:
为了避免死锁,可以采取以下措施:
ThreadLocal 为每个使用该变量的线程提供独立的变量副本,每个线程都可以独立地修改自己的副本,而不会影响其他线程的副本。其原理如下:
每个 Thread 对象内部都有一个 ThreadLocalMap 类型的成员变量threadLocals,当线程调用 ThreadLocal 的set方法时,实际上是将当前线程作为键,要设置的值作为值,存入该线程的threadLocals中。例如:
ThreadLocal
threadLocal.set("value for this thread");
这里,threadLocal在当前线程的threadLocals中存入了一个键值对,键为threadLocal对象本身,值为"value for this thread"。
当线程调用get方法时,会先获取当前线程的threadLocals,然后根据当前 ThreadLocal 对象作为键去查找对应的值并返回。如果在threadLocals中没有找到对应的键值对(即该线程尚未对这个 ThreadLocal 进行set操作),则会调用initialValue方法来获取初始值(initialValue方法默认返回null,可以通过继承 ThreadLocal 并重写initialValue方法来自定义初始值)。
ThreadLocalMap 是 ThreadLocal 的一个内部类,它使用线性探测法来解决哈希冲突(不同于 HashMap 的链地址法)。在 ThreadLocalMap 中,Entry 类继承自 WeakReference
线程池的主要作用如下:
String url = "jdbc:mysql://localhost:3306/mydb";
String username = "root";
String password = "password";
Connection connection = DriverManager.getConnection(url, username, password);
String sql = "SELECT * FROM users WHERE age >?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 30);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
// 处理查询结果
}
finally {
try {
if (resultSet!= null) resultSet.close();
if (preparedStatement!= null) preparedStatement.close();
if (connection!= null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
String sql = "INSERT INTO users (name, age) VALUES (?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "John");
preparedStatement.setInt(2, 30);
preparedStatement.executeUpdate();
String sql = "INSERT INTO products (name, price) VALUES (?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (Product product : productList) {
preparedStatement.setString(1, product.getName());
preparedStatement.setDouble(2, product.getPrice());
preparedStatement.addBatch();
}
preparedStatement.executeBatch();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
resultSet.setFetchSize(100);
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("root");
config.setPassword("password");
HikariDataSource dataSource = new HikariDataSource(config);
Connection connection = dataSource.getConnection();
优化 SQL 查询语句可从以下几个方面提高数据库性能:
CREATE INDEX idx_users_email ON users (email);
索引可以加快数据的查找速度,因为数据库可以通过索引快速定位到满足条件的数据行,而不需要全表扫描。
SELECT user_id, (SELECT COUNT(*) FROM orders WHERE user_id = users.user_id) AS order_count
FROM users;
可以改写为JOIN方式:
SELECT users.user_id, COUNT(orders.order_id) AS order_count
FROM users
LEFT JOIN orders ON users.user_id = orders.user_id
GROUP BY users.user_id;
JOIN操作通常比子查询更高效,因为数据库可以更好地优化JOIN操作的执行计划。
SELECT products.product_name, stocks.quantity
FROM products
INNER JOIN stocks ON products.product_id = stocks.product_id;
如果要查询所有商品及其库存情况(包括没有库存的商品),使用LEFT JOIN:
SELECT products.product_name, stocks.quantity
FROM products
LEFT JOIN stocks ON products.product_id = stocks.product_id;