Java 基础面试题

16. ArrayList 和 Vector 的区别是什么?

ArrayList 和 Vector 均为 List 接口的实现类,底层均基于动态数组实现,但二者在线程安全性能扩容机制上存在核心差异,具体区别如下:

  • 线程安全:Vector 使用 synchronized 关键字实现线程同步,是线程安全的;而 ArrayList 未做同步处理,是非线程安全的,多线程并发修改可能导致数据错乱(如元素覆盖、数组索引越界)。
  • 性能:由于 Vector 存在同步锁开销,在单线程环境下,ArrayList 的读写性能远高于 Vector;即使在多线程环境,也更推荐使用 ConcurrentHashMap 等并发集合替代 Vector(性能更优)。
  • 扩容机制:二者都会根据实际需求动态调整容量,但扩容策略不同:Vector 扩容时,默认将容量翻倍(若未指定扩容增量,newCapacity = oldCapacity * 2);ArrayList 扩容时,默认将容量增加 50%(newCapacity = oldCapacity + (oldCapacity >> 1))。

17. Array 和 ArrayList 有何区别?

Array(数组)和 ArrayList 是 Java 中两种常用的存储结构,核心区别体现在存储数据类型容量灵活性功能丰富度上:

  • 存储数据类型:Array 既可以存储基本数据类型(如 int[]char[]),也可以存储引用类型(如 String[]Object[]);而 ArrayList 只能存储引用类型(若需存储基本类型,需使用其包装类,如 ArrayList<Integer>)。
  • 容量灵活性:Array 是固定大小的,在初始化时必须指定容量,后续无法动态调整(若需扩容,需手动创建新数组并复制元素);ArrayList 是动态扩容的,初始化时可指定容量,也可使用默认容量(16),当元素数量超过容量时会自动扩容,无需手动处理。
  • 功能丰富度:Array 仅提供 length 属性获取长度,无其他内置操作方法;ArrayList 继承了 Collection 接口,提供了丰富的操作方法,如 addAll()(批量添加)、removeAll()(批量删除)、contains()(判断元素是否存在)、iterator()(迭代器遍历)等,开发效率更高。

18. 在 Queue 中 poll () 和 remove () 有什么区别?

poll () 和 remove () 均为 Queue 接口的方法,作用都是 “返回队列的第一个元素,并将该元素从队列中删除”,但二者在无元素时的处理方式上存在关键差异:

  • 相同点:当队列中有元素时,调用 poll () 或 remove () 都会返回队首元素,并移除该元素,执行逻辑一致。
  • 不同点:若队列中无元素(为空),poll () 会返回 null,不会抛出异常;而 remove () 会直接抛出 NoSuchElementException 异常,需通过异常处理机制捕获。

代码示例

java

运行

import java.util.LinkedList;
import java.util.Queue;
import java.util.NoSuchElementException;

public class QueuePollRemoveDemo {
    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<>();
        queue.offer("first"); // 向队列添加元素
        
        // 队列有元素时,二者行为一致
        System.out.println(queue.poll()); // 输出:first,队列变为空
        // 队列无元素时,poll()返回null
        System.out.println(queue.poll()); // 输出:null
        
        // 队列无元素时,remove()抛出异常
        try {
            queue.remove();
        } catch (NoSuchElementException e) {
            System.out.println("异常:队列无元素,remove()无法执行");
        }
    }
}

19. 创建线程有哪几种方式?

在 Java 中,创建线程的核心方式有四种,分别基于不同的接口或类实现,具体如下:

  1. 继承 Thread 类,重写 run () 方法:Thread 类实现了 Runnable 接口,继承后需重写 run () 方法定义线程执行逻辑,通过调用 start() 方法启动线程(而非直接调用 run (),否则会以普通方法形式同步执行)。代码示例

    java

    运行

    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("线程1执行:继承Thread类");
        }
    }
    // 启动线程
    new MyThread().start();
    
  2. 实现 Runnable 接口,重写 run () 方法:Runnable 接口仅定义 run () 方法,实现类需重写该方法,再将实现类对象作为参数传入 Thread 构造器,通过 Thread 对象的 start() 方法启动线程。该方式避免了单继承的限制,更推荐使用。代码示例

    java

    运行

    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("线程2执行:实现Runnable接口");
        }
    }
    // 启动线程
    new Thread(new MyRunnable()).start();
    
  3. 实现 Callable 接口,重写 call () 方法:Callable 接口与 Runnable 类似,但 call () 方法有返回值且可抛出异常,需结合 FutureTask 类(实现 Future 接口)使用,通过 FutureTask 的 get() 方法获取线程执行结果。代码示例

    java

    运行

    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "线程3执行:实现Callable接口,返回结果";
        }
    }
    // 启动线程并获取结果
    FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
    new Thread(futureTask).start();
    System.out.println(futureTask.get()); // 输出线程返回结果
    
  4. 使用线程池创建线程:通过线程池(如 ThreadPoolExecutorExecutors 工具类创建的线程池)管理线程生命周期,避免频繁创建销毁线程的开销。只需将任务(Runnable/Callable 实现类)提交给线程池,线程池会自动分配线程执行任务。代码示例

    java

    运行

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ThreadPoolDemo {
        public static void main(String[] args) {
            // 创建固定大小的线程池
            ExecutorService executorService = Executors.newFixedThreadPool(2);
            // 提交任务
            executorService.submit(() -> System.out.println("线程4执行:线程池创建"));
            // 关闭线程池
            executorService.shutdown();
        }
    }
    

20. 在 Java 程序中怎么保证多线程的运行安全(多线程并发写共享数据)?

多线程并发写共享数据时,需避免数据竞争(多个线程同时修改同一数据导致结果异常),核心保证线程安全的方式有三种:

  1. 使用线程安全类:Java 提供了 java.util.concurrent 包,包含多种线程安全的工具类,可直接用于共享数据操作,无需手动处理同步。例如:
    • AtomicInteger:原子整数类,支持原子化的 incrementAndGet()(自增)、decrementAndGet()(自减)等操作,避免并发修改问题;
    • ArrayBlockingQueue:阻塞队列,支持多线程环境下的安全入队、出队操作,实现线程间数据传递。
  2. 使用自动锁 synchronized:synchronized 是 Java 内置的同步锁,可修饰方法或代码块,确保同一时间只有一个线程进入同步区域执行代码,从而保证共享数据的原子性、可见性和有序性。代码示例(同步代码块)

    java

    运行

    class SafeCounter {
        private int count = 0;
        // 共享数据修改方法
        public void increment() {
            // 锁定当前对象,确保同一时间仅一个线程执行
            synchronized (this) {
                count++;
            }
        }
        public int getCount() { return count; }
    }
    
  3. 使用手动锁 Lock:Lock 是 java.util.concurrent.locks 包下的接口,提供比 synchronized 更灵活的锁控制(如可中断锁、超时锁、读写分离锁),需手动调用 lock() 加锁和 unlock() 解锁(通常在 finally 块中解锁,避免锁泄漏)。常用实现类为 ReentrantLock(可重入锁)。代码示例

    java

    运行

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    class SafeCounterWithLock {
        private int count = 0;
        private Lock lock = new ReentrantLock();
        public void increment() {
            lock.lock(); // 加锁
            try {
                count++; // 共享数据修改
            } finally {
                lock.unlock(); // 解锁(确保无论是否异常都执行)
            }
        }
        public int getCount() { return count; }
    }
    

21. 什么是死锁?

死锁是多线程并发场景中的一种阻塞状态,指两个或多个线程互相持有对方所需的独占资源(锁),且均不主动释放已持有的资源,导致所有线程都无法继续执行,陷入永久阻塞。核心产生条件

  • 线程 A 持有独占锁 a,同时尝试获取线程 B 持有的独占锁 b;
  • 线程 B 持有独占锁 b,同时尝试获取线程 A 持有的独占锁 a;
  • 二者均不释放已持有的锁,形成循环等待,最终导致死锁。

代码示例(死锁场景)

java

运行

class DeadLockDemo {
    // 定义两个独占锁对象
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        // 线程1:持有lockA,尝试获取lockB
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程1持有lockA,尝试获取lockB");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockB) {
                    System.out.println("线程1获取lockB,执行完成");
                }
            }
        }).start();

        // 线程2:持有lockB,尝试获取lockA
        new Thread(() -> {
            synchronized (lockB) {
                System.out.println("线程2持有lockB,尝试获取lockA");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockA) {
                    System.out.println("线程2获取lockA,执行完成");
                }
            }
        }).start();
    }
}

执行结果:两个线程均会卡在尝试获取对方锁的步骤,无法继续执行,形成死锁。

22. 怎么防止死锁?

防止死锁的核心思路是破坏死锁产生的必要条件(如循环等待、永久持有锁),常用方法如下:

  1. 使用带超时的锁尝试机制:避免线程永久等待锁,通过 tryLock(long timeout, TimeUnit unit) 方法(如 ReentrantLockReentrantReadWriteLock 支持)尝试获取锁,若超时未获取到则主动释放已持有的锁,退出等待。代码示例

    java

    运行

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReentrantLock;
    
    class AvoidDeadLockWithTryLock {
        private static final ReentrantLock lockA = new ReentrantLock();
        private static final ReentrantLock lockB = new ReentrantLock();
    
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    // 尝试获取lockA,超时时间1秒
                    if (lockA.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            System.out.println("线程1持有lockA,尝试获取lockB");
                            // 尝试获取lockB,超时时间1秒
                            if (lockB.tryLock(1, TimeUnit.SECONDS)) {
                                try {
                                    System.out.println("线程1获取lockB,执行完成");
                                } finally {
                                    lockB.unlock(); // 释放lockB
                                }
                            } else {
                                System.out.println("线程1获取lockB超时,释放lockA");
                            }
                        } finally {
                            lockA.unlock(); // 释放lockA
                        }
                    } else {
                        System.out.println("线程1获取lockA超时");
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }
    }
    
  2. 使用并发工具类替代手动锁:优先使用 Java 提供的 java.util.concurrent 包下的并发类(如 ConcurrentHashMapCountDownLatch),这些类内部已优化锁机制,避免死锁风险,无需手动设计锁逻辑。
  3. 降低锁的使用粒度与减少同步代码块
    • 降低锁粒度:将一个大锁拆分为多个小锁(如 ConcurrentHashMap 的分段锁),减少线程对同一锁的竞争;
    • 减少同步代码块:仅在修改共享数据的核心逻辑处加锁,避免锁覆盖无关代码,缩短线程持有锁的时间。

23. 什么是 Java 序列化?什么情况下需要序列化?

23.1 定义

Java 序列化是一种将对象在内存中的状态(属性值、类型信息)转换为字节序列的过程;反序列化则是将字节序列恢复为内存中对象的过程。序列化的核心目的是实现对象的 “持久化存储” 和 “跨进程传输”。

23.2 需要序列化的场景

  • 对象状态持久化到存储介质:需将内存中的对象状态保存到文件、数据库等存储介质时(如保存用户会话信息到文件),需先序列化对象为字节序列,再写入存储介质;读取时通过反序列化恢复对象。
  • 对象通过网络传输:需通过套接字(Socket)在网络中传输对象时(如分布式系统中跨节点传递对象、RPC 远程方法调用),需先序列化对象为字节流,通过网络发送;接收方再反序列化字节流为对象。
  • 对象通过 RMI 传输:Java 远程方法调用(RMI)中,客户端与服务器端之间传递的对象需实现序列化,否则无法跨进程传输。

23.3 注意事项

  • 被序列化的类需实现 java.io.Serializable 接口(标记接口,无任何抽象方法,仅用于标识该类可序列化);
  • 若类中存在不需要序列化的字段,需用 transient 关键字修饰(序列化时会忽略该字段,反序列化时该字段为默认值,如基本类型为 0,引用类型为 null);
  • 静态成员变量不会被序列化(静态变量属于类,不属于对象状态,序列化仅处理对象的实例属性)。

24. Error 和 Exception 区别是什么?

Error 和 Exception 均为 java.lang.Throwable 类的子类,是 Java 异常体系的两大核心类型,但二者在错误性质处理方式适用场景上存在本质区别:

对比维度ErrorException
错误性质虚拟机级别的严重错误,如系统崩溃、内存不足、堆栈溢出等,属于 “不可恢复的错误”应用程序级别的异常,如空指针、数组越界、文件未找到等,属于 “可恢复的异常”
编译期检测编译器不检测 Error,无法在编译阶段预知分为受检异常(如 IOException)和非受检异常(如 RuntimeException),受检异常需编译器强制处理
处理方式应用程序不应捕获和处理 Error(捕获后也无法恢复),通常会导致程序终止应用程序需捕获并处理 Exception(如通过 try-catch 块),处理后程序可继续执行
示例OutOfMemoryError(内存不足)、StackOverflowError(堆栈溢出)NullPointerException(空指针)、FileNotFoundException(文件未找到)

25. 运行时异常和一般异常(受检异常)区别是什么?

运行时异常和一般异常(受检异常)均为 Exception 的子类,核心区别在于编译器强制处理要求使用场景

25.1 运行时异常(非受检异常)

  • 定义:继承自 RuntimeException 类及其子类的异常,代表 JVM 运行期间可能出现的异常(如空指针、数组越界)。
  • 编译器处理:编译器不强制要求捕获或声明该类异常,即使未处理,程序也可通过编译;若运行时发生该异常且未捕获,会导致线程终止并抛出异常栈信息。
  • 示例NullPointerException(空指针访问)、ArrayIndexOutOfBoundsException(数组索引越界)、ClassCastException(类型转换异常)。
  • 使用建议:通常用于表示程序逻辑错误(如参数非法),无需强制处理,推荐通过优化代码逻辑避免(如判断对象非 null 后再访问)。

25.2 一般异常(受检异常)

  • 定义:Exception 类中除 RuntimeException 及其子类之外的异常,代表程序运行中可预知的异常(如文件操作、网络连接异常)。
  • 编译器处理:编译器强制要求处理该类异常,若未通过 try-catch 捕获或在方法签名中用 throws 声明,程序无法通过编译。
  • 示例IOException(IO 操作异常)、SQLException(数据库操作异常)、ClassNotFoundException(类未找到异常)。
  • 使用建议:通常用于表示外部环境异常(如文件不存在、网络断开),需强制处理,确保程序在异常发生后可优雅恢复(如提示用户 “文件不存在” 并重新选择文件)。

25.3 核心区别总结

对比维度运行时异常(非受检异常)一般异常(受检异常)
父类RuntimeExceptionException(非 RuntimeException 分支)
编译器强制处理否(无需捕获或声明)是(必须捕获或声明)
异常来源程序逻辑错误(如空指针)外部环境异常(如文件未找到)
处理原则优化逻辑避免异常发生强制捕获并处理,确保程序恢复

26. throw 和 throws 的区别是什么?

throw 和 throws 均用于 Java 异常处理,但其作用位置功能使用方式完全不同,具体区别如下:

对比维度throwthrows
作用位置方法内部(代码块中)方法声明处(方法名后,参数列表与方法体之间)
功能主动抛出一个具体的异常对象(触发异常)声明该方法可能抛出的异常列表(告知调用者异常风险)
抛出对象数量一次只能抛出一个异常对象(如 throw new NullPointerException()可声明多个异常(用逗号分隔,如 throws IOException, SQLException
异常类型可抛出受检异常和非受检异常声明的异常可为受检异常或非受检异常(受检异常需调用者处理)
使用示例if (obj == null) throw new NullPointerException();public void readFile() throws FileNotFoundException {}

代码示例(结合使用)

java

运行

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class ThrowThrowsDemo {
    // 方法声明可能抛出FileNotFoundException(受检异常)
    public static String readFile(String path) throws FileNotFoundException {
        File file = new File(path);
        if (!file.exists()) {
            // 方法内部主动抛出具体异常对象
            throw new FileNotFoundException("文件不存在:" + path);
        }
        // 读取文件逻辑(省略)
        return new Scanner(file).nextLine();
    }

    public static void main(String[] args) {
        // 调用readFile(),需处理其声明的FileNotFoundException
        try {
            readFile("test.txt");
        } catch (FileNotFoundException e) {
            System.out.println("处理异常:" + e.getMessage());
        }
    }
}

27. try-catch-finally 中哪个部分可以省略?

在 try-catch-finally 结构中,catch 块可以省略,但需满足特定条件;try 块是核心,不可省略;finally 块也可省略,但通常用于资源清理,建议保留。具体规则如下:

27.1 可省略的情况:catch 块

  • 适用场景:仅处理运行时异常(非受检异常),且无需显式捕获异常(如仅需通过 finally 块做资源清理,不关心异常具体信息)。此时可省略 catch 块,直接用 try-finally 结构。代码示例

    java

    运行

    public class TryFinallyDemo {
        public static void main(String[] args) {
            Scanner scanner = null;
            try {
                scanner = new Scanner(new File("test.txt"));
                System.out.println(scanner.nextLine()); // 可能抛出运行时异常(如NoSuchElementException)
            } finally {
                // 无论是否发生异常,均关闭资源
                if (scanner != null) {
                    scanner.close();
                }
            }
        }
    }
    
  • 注意:若 try 块中可能抛出受检异常(如 FileNotFoundException),则不能省略 catch 块,也需在方法签名中用 throws 声明该异常,否则编译器报错(受检异常必须强制处理)。

27.2 不可省略的情况:try 块

try 块是异常处理的核心,用于包裹可能抛出异常的代码,必须存在,否则 catch 和 finally 块无意义(无代码可监控异常,也无资源需清理)。

27.3 可省略的情况:finally 块

若无需在异常发生前后执行固定清理逻辑(如关闭文件、释放锁),则可省略 finally 块,直接用 try-catch 结构。但通常建议保留 finally 块处理资源清理,避免资源泄漏。

28. == 和 equals 的区别是什么?

== 和 equals 均用于判断 “相等性”,但二者判断维度适用类型完全不同,具体区别如下:

28.1 == 运算符

  • 判断维度:判断两个对象的内存地址是否相同(即是否为同一对象);对于基本数据类型,判断值是否相同(因基本数据类型直接存储值,无内存地址概念)。
  • 适用类型
    • 基本数据类型(如 intchar):比较值是否相等(如 1 == 1 为 true);
    • 引用数据类型(如 StringObject):比较内存地址是否相同(如 new Object() == new Object() 为 false,因两个对象在堆中地址不同)。

28.2 equals () 方法

  • 定义equals() 是 Object 类的方法,默认实现与 == 一致(比较引用地址),但多数类会重写该方法,改为比较对象的 “内容是否相等”。
  • 适用类型:仅适用于引用数据类型(基本数据类型无方法,需通过包装类调用 equals ())。
  • 两种使用情况
    1. 类未重写 equals():等价于 ==,比较引用地址(如自定义类未重写时,new MyClass() == new MyClass() 为 false);
    2. 类重写 equals():比较对象内容(如 String 类重写后,"abc".equals("abc") 为 true,即使是不同对象,只要内容相同则返回 true)。

代码示例

java

运行

public class EqualsDemo {
    public static void main(String[] args) {
        // 1. 基本数据类型:==比较值
        int a = 10;
        int b = 10;
        System.out.println(a == b); // 输出:true

        // 2. 引用数据类型:==比较地址
        String s1 = new String("abc");
        String s2 = new String("abc");
        System.out.println(s1 == s2); // 输出:false(不同对象,地址不同)
        System.out.println(s1.equals(s2)); // 输出:true(String重写equals,比较内容)

        // 3. 未重写equals的自定义类
        class MyClass {}
        MyClass obj1 = new MyClass();
        MyClass obj2 = new MyClass();
        System.out.println(obj1.equals(obj2)); // 输出:false(默认比较地址)
    }
}

29. Java 中 IO 流分为几种?

Java IO 流可根据不同维度划分为三类,具体分类如下:

29.1 按流的流向划分

  • 输入流(InputStream/Reader):数据从外部存储介质(如文件、网络)流向内存的流,用于读取数据(如 FileInputStream 从文件读取字节到内存)。
  • 输出流(OutputStream/Writer):数据从内存流向外部存储介质的流,用于写入数据(如 FileOutputStream 将内存中的字节写入文件)。

29.2 按操作单元划分

  • 字节流(InputStream/OutputStream):以字节(8 位)为操作单元的流,可处理任意类型的数据(如文本、图片、视频),核心类包括 FileInputStreamFileOutputStreamBufferedInputStream
  • 字符流(Reader/Writer):以字符(16 位,适配 Unicode 编码)为操作单元的流,仅用于处理文本数据(如 .txt 文件),可自动处理字符编码转换,核心类包括 FileReaderFileWriterBufferedReader

29.3 按流的角色划分

  • 节点流(直接流):直接与数据源(如文件、网络端口)连接的流,直接操作数据源,无中间缓冲层(如 FileInputStream 直接读取文件,SocketInputStream 直接读取网络数据)。
  • 处理流(包装流):基于节点流或其他处理流包装而成的流,用于增强节点流的功能(如缓冲、编码转换),不直接连接数据源(如 BufferedInputStream 为节点流添加缓冲,减少 IO 次数;InputStreamReader 将字节流转换为字符流)。

30. 什么是反射机制?

30.1 定义

Java 反射机制是指程序在运行状态中,对于任意一个类,能够动态获取该类的所有属性(成员变量)、方法(构造方法、普通方法)、接口等信息;对于任意一个对象,能够动态调用其任意方法和修改其任意属性的能力。反射打破了 Java 编译期的类型绑定限制,实现 “动态编译”(运行时确定类型和绑定对象)。

30.2 静态编译与动态编译的对比

  • 静态编译:编译期确定类的类型和对象的绑定关系,代码执行逻辑固定(如 Student stu = new Student();,编译期已确定 stu 为 Student 类型)。
  • 动态编译:运行期才确定类的类型和对象的绑定关系,代码执行逻辑可动态调整(如通过反射加载 Student 类,运行时才创建对象,无需编译期知晓 Student 类)。

30.3 反射的核心价值

  • 框架开发:多数 Java 框架(如 Spring、MyBatis)基于反射实现对象创建、依赖注入、配置解析(如 Spring 通过反射创建配置文件中指定的 Bean 对象);
  • 动态代理:实现 AOP(面向切面编程)时,通过反射动态生成代理类,增强目标方法的功能(如事务管理、日志记录);
  • 工具开发:开发 IDE、调试工具时,通过反射获取类的结构信息(如属性、方法),实现代码提示、断点调试等功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值