JAVA基础面试题——最常见问题(3)

发布于 2025-09-03 16:08:30 浏览 20 次

Java 基础面试题(3)

1.Java 的 I/O 流是什么?

JavaIO流是Java语言中用于处理数据传输的核心机制,主要通过输入流和输出流实现数据读取与写入。其分类依据包括流向(输入/输出)、数据单位(字节/字符)以及功能(节点流/处理流),形成多层次的数据处理体系。

JavaIO流围绕四大抽象类构建:字节流对应InputStream和OutputStream,提供基础的字节读写方法;字符流基于Reader和Writer,支持文本数据处理。核心方法涵盖数据读取(如read())、写入(如write())、资源释放(close())以及缓冲区管理(flush()),确保高效稳定的数据操作。
作为Java标准库的重要组成部分,JavaIO流自Java早期版本即被纳入基础类库,其设计逐步优化以适应多样化的I/O需求,成为Java生态中数据交互的基础设施。

2.什么是 Java 的网络编程?

Java 的网络编程是指利用 Java 语言及其提供的 java.net 等相关类库,实现不同计算机或程序之间通过网络进行数据传输与通信的技术,它封装了 TCP、UDP 等底层网络协议的复杂细节,提供了如 Socket、ServerSocket(用于 TCP 连接)和 DatagramSocket、DatagramPacket(用于 UDP 数据报)等核心类,以及 URL、URLConnection 等简化高层协议操作的工具,使开发者能够便捷地构建客户端与服务器之间的通信链路 —— 无论是面向连接、可靠传输的 TCP 通信(适用于文件传输、登录验证等场景),还是无连接、高效但不保证可靠性的 UDP 通信(适用于实时音视频、游戏数据等场景),都可以通过这些 API 实现;通过网络编程,Java 程序能够跨设备交换数据,是开发分布式应用、即时通讯工具、网络爬虫、物联网交互等场景的基础,它屏蔽了硬件和操作系统的差异,让开发者专注于业务逻辑,从而构建出灵活、可扩展的网络应用。

3.Java 中的基本数据类型有哪些?

Java 中的基本数据类型(Primitive Data Types)共有 8 种,可分为四大类:
·整数类型:byte(1 字节,范围 -128~127)、short(2 字节,-32768~32767)、int(4 字节,-2³¹~2³¹-1,最常用)、long(8 字节,-2⁶³~2⁶³-1,声明时需加后缀 L 或 l);
·浮点类型:float(4 字节,单精度,声明时需加后缀 F 或 f)、double(8 字节,双精度,默认浮点类型);
·字符类型:char(2 字节,存储 Unicode 字符,范围 u0000~uffff,用单引号表示);
·布尔类型:boolean(无明确字节数,仅表示 true 或 false,用于逻辑判断)。

这些类型直接存储数据值,而非对象引用,具有占用空间小、操作效率高的特点,是 Java 中最基础的数据存储单元。

4.什么是 Java 中的自动装箱和拆箱?

Java 中的自动装箱和拆箱是语言特性,用于基本数据类型与包装类之间的自动转换,而网络编程是Java的另一重要应用领域。

自动装箱与拆箱:
自动装箱是将基本数据类型(如int、double等)自动转换为对应的包装类对象(如Integer、Double),而自动拆箱则是将包装类对象转换回基本数据类型。这一特性简化了代码编写,避免了手动装箱和拆箱的繁琐操作,同时增强了代码安全性。 ‌

网络编程:
网络编程是Java的另一核心应用领域,主要用于实现客户端与服务器之间的数据传输。在Java中,网络编程通常涉及 Socket 编程、 HTTP 通信等,需处理IP地址、端口号等网络参数,并使用 TCP/IP协议 等实现数据传输。

5.什么是 Java 中的迭代器(Iterator)?

Java 中的迭代器(Iterator)是一种用于遍历集合(如 List、Set 等)元素的接口,它提供了统一的遍历方式,屏蔽了不同集合底层数据结构的差异。迭代器接口定义了三个核心方法:hasNext() 用于判断集合中是否还有未遍历的元素,返回布尔值;next() 用于获取下一个元素,并将迭代器指针向后移动;remove() 用于删除当前迭代器指向的元素(可选操作)。使用时,通过集合的 iterator() 方法获取迭代器实例,然后通过 hasNext() 判断是否有元素,再用 next() 逐个获取元素,实现对集合的遍历。这种方式相比传统的 for 循环更灵活,尤其适合在遍历过程中需要删除元素的场景,同时遵循 “迭代器模式”,使集合的遍历与集合本身的实现解耦,增强了代码的可维护性和扩展性。

6.Java 运行时异常和编译时异常之间的区别是什么?

Java 中的运行时异常(RuntimeException)和编译时异常(Checked Exception)是两种不同类型的异常,核心区别体现在编译阶段的检查机制和使用场景上:编译时异常是编译器强制要求处理的异常(如 IOException、SQLException),这类异常通常由外部环境因素导致(如文件不存在、数据库连接失败),编译器会检查代码中是否通过 try-catch 捕获或 throws 声明抛出,若未处理则编译失败,目的是提醒开发者预先考虑可能出现的异常情况;而运行时异常是编译器不强制处理的异常(如 NullPointerException、IndexOutOfBoundsException),通常由程序逻辑错误导致(如空指针访问、数组越界),这类异常在编译时不会报错,但可能在程序运行时触发,其设计意图是让开发者通过完善逻辑来避免,而非强制捕获。简单来说,编译时异常是 “必须处理的外部异常”,运行时异常是 “应通过代码优化避免的逻辑异常”。

7.什么是 Java 中的继承机制?

Java 中的继承机制是通过extends关键字实现类之间的层次关系,允许子类继承父类的属性和方法,同时可扩展新特性或重写父类方法。 ‌

核心概念
继承是面向对象编程的核心特性之一,通过extends关键字定义子类与父类之间的“is-a”关系(即子类是父类的一种特殊类型)。例如,Teacher类继承Person类,表示每位教师本质上也是一个人。 ‌

实现方式
‌语法‌:使用extends关键字,如class Child extends Parent。
‌属性继承‌:子类可直接使用父类的属性(如name、age),无需重新定义。
‌方法继承‌:子类可直接调用父类方法(如print()),也可重写或添加新方法。 ‌

限制与作用
‌单继承‌:Java不支持多继承(即一个子类不能同时继承多个父类),但可通过接口实现多继承的类似效果。 ‌

‌作用‌:
代码复用‌:避免重复编写通用代码。
‌扩展性‌:子类可新增功能或修改父类行为。
‌维护性‌:修改父类代码可统一更新所有子类。 ‌

典型应用
‌通用基类‌:如“动物”基类派生出“猫”“狗”等子类。
‌层次化设计‌:通过分类组织代码结构,增强清晰度。

8.什么是 Java 的封装特性?

Java封装 是面向对象编程的核心特性之一,通过隐藏对象的内部细节和实现细节,仅通过公共接口提供访问权限,以保护数据安全并降低模块间的耦合度。 ‌

核心概念
封装通过访问控制修饰符(如private、protected、public)将类的属性(数据)和方法(行为)结合,仅允许通过预定义的公共接口访问数据。例如,将银行卡密码声明为private,并通过getBalance等公共方法提供受控访问。 ‌

实现方式
‌私有化属性‌:使用private关键字隐藏内部数据,外部无法直接访问。
‌提供公共方法‌:通过public的getter(如getName())和setter(如setAge())控制数据访问,并在方法中加入逻辑验证(如数据校验)。 ‌

‌构造方法‌:在创建对象时初始化私有属性(如BankAccount(accountNumber, ownerName, initialBalance))。 ‌

优点
‌安全性‌:防止外部代码直接修改敏感数据。
‌可维护性‌:修改内部实现时无需修改外部调用代码。
‌可重用性‌:封装后的类可作为独立模块重复使用。 ‌

示例代码:

public class Person {
    private String name;
    private int age;
    private double salary;
    private String job;

    public Person(String name, int age, double salary, String job) {
        this.setName(name);
        this.setAge(age);
        this.setSalary(salary);
        this.setJob(job);
    }

    public String getName() { return name; }
    public void setName(String name) { if (name.length() >= 2 && name.length() <= 6) this.name = name; else this.name = "无名客"; }
    // 其他属性设置类似...
}

9.Java 中的访问修饰符有哪些?

·public(公共访问权限):权限范围最广,被 public 修饰的元素(类、变量、方法等)可以在任意包、任意类中被访问,不受包和类的限制。例如,Java 标准库中的 java.util.ArrayList 类就是 public 的,因此任何项目都能直接导入并使用它;其 add()、get() 等方法也为 public,允许外部代码自由调用。

·protected(受保护访问权限):权限范围次之,被 protected 修饰的元素只能在当前类、当前类的子类(无论子类是否在同一包中)、同一包中的其他类中访问,无法被不同包且无继承关系的类访问。它的核心作用是 “保护父类的核心成员,仅允许子类继承和复用”,例如父类 Animal 中定义 protected void eat(),则子类 Dog(无论是否同包)和同包的 Plant 类可调用 eat(),但不同包的 Car 类无法调用。
默认修饰符(无显式修饰符,包访问权限):若未给类、变量、方法添加任何访问修饰符,则为默认权限,其可见范围仅限于当前包中的所有类,不同包的类(即使是子类)也无法访问。这种修饰符适合 “包内组件间的协作”,例如一个包中多个工具类相互调用的辅助方法,无需暴露给外部包,就可使用默认权限,避免不必要的外部依赖。

·private(私有访问权限):权限范围最小,被 private 修饰的元素仅能在当前类内部访问,当前类的子类、同一包的其他类、不同包的类均无法访问。它是 “封装思想的核心体现”,用于隐藏类的内部实现细节,仅通过 public 或 protected 的方法对外提供有限接口,例如类 Person 中定义 private String password,外部无法直接修改

password,只能通过 public void setPassword(String pwd) 方法间接修改,确保密码的安全性和合法性校验。

10.Java 中静态方法和实例方法的区别是什么?

Java 中静态方法(Static Method)和实例方法(Instance Method)的核心区别体现在调用方式、所属对象、访问权限和使用场景上:静态方法通过 static 关键字修饰,属于类本身,不依赖于类的实例,可直接通过 “类名。方法名” 调用,且内部只能访问静态成员(静态变量、静态方法),无法访问实例成员或使用 this 关键字;而实例方法属于类的具体实例,必须通过实例对象调用(如 “对象名。方法名”),内部可以访问实例成员和静态成员,也能使用 this 引用当前实例。从设计意图看,静态方法通常用于实现工具类功能(如 Math.random())或与类整体相关的操作,不依赖于对象的状态;实例方法则与对象的状态紧密相关,用于操作实例的属性或实现对象的行为(如 String 的 length() 方法)。简单来说,静态方法属于 “类级别”,实例方法属于 “对象级别”,二者在调用方式和访问权限上的差异,反映了它们在面向对象设计中分别对应类的共性功能和对象的个性行为。

11.Java 中 for 循环与 foreach 循环的区别是什么?

Java 中 for 循环与 foreach 循环(增强 for 循环)的核心区别体现在语法结构、适用场景和功能特性上:for 循环是传统的循环结构,通过索引(如数组下标)或计数器控制循环次数,语法为 for(初始化; 条件; 迭代),支持在循环中修改循环变量、控制循环流程(如使用 break 跳出整个循环、continue 跳过当前迭代),且能灵活访问数组或集合的特定索引位置元素,适用于需要精确控制循环过程或操作索引的场景;而 foreach 循环是简化的循环形式,语法为 for(元素类型 变量名 : 数组/集合),无需关心索引,直接遍历容器中的每个元素,代码更简洁,但无法获取元素索引,也不能在循环中修改容器的结构(如添加 / 删除元素,否则可能抛出 ConcurrentModificationException),且不支持 break/continue 之外的复杂流程控制,适用于单纯遍历元素、无需操作索引的场景。简单来说,for 循环更灵活,适合需要控制循环细节的场景;foreach 循环更简洁,适合快速遍历元素的场景。

12.什么是 Java 中的双亲委派模型?

Java 中的双亲委派模型是 Java 类加载机制的核心原则,用于规范类加载器加载类的流程。其核心思想是:当一个类加载器需要加载某个类时,它不会首先自己尝试加载,而是将加载请求委派给其父类加载器,只有当父类加载器无法完成加载(即父类加载器在自己的搜索范围内找不到该类)时,子加载器才会尝试自己去加载。

这一模型通过层级关系组织类加载器(从顶层到底层通常包括启动类加载器、扩展类加载器、应用程序类加载器以及自定义类加载器),确保了类加载的有序性和安全性:一方面,它避免了类的重复加载(父类已加载的类,子类无需再加载);另一方面,它防止了核心类被篡改(例如,用户自定义的 java.lang.String 类不会被加载,因为启动类加载器已优先加载了 JDK 中的核心 String 类),从而保证了 Java 核心库的安全性和稳定性。

13.Java 中 wait() 和 sleep() 的区别?

Java 中 wait() 和 sleep() 都可用于暂停线程执行,但二者在所属类、作用机制、锁处理和使用场景上有显著区别:

· 所属类不同:wait() 是 Object 类的方法,而 sleep() 是 Thread 类的静态方法。

· 锁的处理不同:wait() 会释放当前线程持有的对象锁,让其他线程有机会获取该锁;sleep() 则不会释放任何锁,即使线程持有锁,休眠期间其他线程也无法获取。

· 唤醒机制不同:wait() 需通过其他线程调用同一对象的 notify() 或 notifyAll() 方法唤醒,或等待指定时间后自动唤醒;sleep() 只需等待设定的时间结束后自动恢复,无需其他线程干预。

· 使用场景不同:wait() 通常用于多线程间的协作(如生产者 - 消费者模型),通过释放锁让其他线程操作共享资源;sleep() 主要用于让线程暂停一段时间(如模拟延迟),不涉及多线程间的锁竞争协调。

此外,wait() 必须在同步代码块(synchronized)中使用,否则会抛出 IllegalMonitorStateException;而 sleep() 可在任何地方使用,无需依赖同步机制。

14.Java 和 Go 的区别

Java 和 Go 是两种不同设计理念的编程语言,在语法特性、内存管理、并发模型、适用场景等方面存在显著区别,主要差异如下:
设计目标与哲学

Java 强调 “一次编写,到处运行”(跨平台性)和面向对象(OOP),语法严谨,注重代码的可维护性和扩展性,适合构建大型复杂的企业级应用。
Go 由 Google 设计,追求 “简单、高效、并发友好”,摒弃了 OOP 中的继承等复杂特性,语法简洁(如无类继承、无泛型早期限制),注重编译速度和运行效率,适合云原生、分布式系统等场景。

语法与编程范式
Java 是纯粹的面向对象语言,强制使用类和对象组织代码,依赖继承、接口、泛型等特性,语法相对繁琐(如必须写 public static void main 作为入口)。
Go 支持面向对象但不强调继承,通过结构体(struct)和接口(interface)实现多态,支持函数式编程特性(如匿名函数、闭包),语法极简(如省略分号、简洁的变量声明 :=),且入口函数为 func main() 无需类包裹。

内存管理
Java 依赖垃圾回收(GC)自动管理内存,开发者无需手动释放,但早期 GC 存在停顿问题(现代 JVM 已优化),且需要关注对象创建开销。
Go 同样采用自动 GC,但设计更轻量(如三色标记法),GC 停顿时间短,且支持指针操作(但不允许指针运算),内存控制更灵活。

并发模型
Java 基于线程(Thread)和锁(synchronized 或 java.util.concurrent 框架)实现并发,线程是操作系统级资源,开销较大,需手动处理锁竞争问题。
Go 引入 “goroutine”(轻量级线程),由 Go 运行时调度,开销极小(初始栈仅 2KB),支持百万级并发;通过 “通道(channel)” 实现 goroutine 间通信,遵循 “不要通过共享内存通信,而通过通信共享内存”,简化并发编程。

生态与应用场景
Java 生态成熟庞大,有 Spring、Hibernate 等企业级框架,广泛用于后端开发、Android 应用、大数据(如 Hadoop)等领域。
Go 生态聚焦云原生和系统级开发,适合微服务(如 Docker、Kubernetes 均用 Go 开发)、网络编程、工具开发等,编译为静态二进制可执行文件,部署简单(无需依赖运行时)。

编译与执行
Java 先编译为字节码(.class),再通过 JVM 解释执行(或 JIT 编译为机器码),跨平台性强但依赖 JVM。
Go 直接编译为机器码,无需运行时依赖,启动速度快,编译产物体积小,适合容器化部署。

15.Java Object 类中有什么方法,有什么作用?

Java 的 java.lang.Object 类是所有类的父类(包括数组),它定义了一组些所有对象都具备的基本方法,这些方法为对象的通用行为提供了标准实现,主要包括以下 11 个核心方法及其作用:

· toString():返回对象的字符串表示(默认格式为 “类名 @哈希码的十六进制”),通常需要子类重写以提供有意义的对象信息(如打印对象属性)。

· equals(Object obj):判断当前对象与参数对象是否 “相等”,默认实现是比较对象地址(即 ==),子类可重写以定义自定义相等逻辑(如 String 类重写后比较字符序列)。

· hashCode():返回对象的哈希码(整数),用于哈希表(如 HashMap)中快速定位对象,需遵循 “相等对象必须有相等哈希码” 的原则,与 equals() 配合使用。

· getClass():返回对象的运行时类(Class 对象),可用于反射操作(如获取类名、方法、属性等元信息)。

· clone():创建并返回对象的副本(浅拷贝),需实现 Cloneable 接口,否则会抛出 CloneNotSupportedException,用于快速复制对象。

· wait() 系列:包括 wait()、wait(long timeout)、wait(long timeout, int nanos),使当前线程释放对象锁并进入等待状态,需在同步代码块中使用,可被 notify() 或超时唤醒,用于多线程协作。

· notify():唤醒在此对象锁上等待的单个线程,使其继续执行。

· notifyAll():唤醒在此对象锁上等待的所有线程,让它们竞争获取锁。

· finalize():垃圾回收器清理对象前调用的方法,用于释放资源(如关闭文件),但因执行时机不确定,现已不推荐使用。

这些方法构成了 Java 对象的基础行为规范,子类可根据需求重写(如 toString()、equals())或直接使用(如 getClass()、wait()),是 Java 面向对象特性的重要支撑。

16.Java 中的字节码是什么?

Java 中的字节码(Bytecode)是一种介于源代码(.java 文件)和机器码之间的中间代码,它是 Java 源代码经过编译器(javac)编译后生成的二进制文件(.class 文件)。字节码不依赖于特定的操作系统或硬件平台,采用与机器码类似的指令集,但指令针对的是 Java 虚拟机(JVM)而非实际的 CPU。

其核心作用体现在 Java 的 “一次编写,到处运行”(Write Once, Run Anywhere)特性中:当程序运行时,JVM 会将字节码解释或编译(通过 JIT 即时编译器)为当前平台的机器码,使同一字节码文件能在安装了 JVM 的任何操作系统(如 Windows、Linux、macOS)上执行,实现了跨平台兼容性。

字节码的指令集包含了操作数栈、局部变量、类成员访问等相关指令,例如加载常量、调用方法、操作对象等,这些指令由 JVM 规范严格定义,确保不同厂商实现的 JVM 都能正确解析和执行相同的字节码。这种中间层设计既保留了编译型语言的执行效率,又具备解释型语言的跨平台灵活性,是 Java 语言的核心特性之一。

17.什么是 BIO、NIO、AIO?

BIO、NIO、AIO 是 Java 中三种不同的 I/O 模型,分别对应阻塞 I/O、非阻塞 I/O 和异步 I/O,它们在处理 I/O 操作的方式上有本质区别,适用于不同的场景:
BIO(Blocking I/O,阻塞 I/O):

传统的 I/O 模型,所有操作(如读取、写入)都是阻塞的。当一个线程执行 I/O 操作时(如从网络读取数据),会一直阻塞直到操作完成(或超时),期间无法做其他事情。为了处理多个并发连接,通常需要为每个连接创建一个独立线程,这会导致线程资源消耗大、扩展性差。
适用场景:连接数少且固定的简单场景(如早期的简单 Socket 通信)。

NIO(Non-blocking I/O,非阻塞 I/O):
JDK 1.4 引入的新 I/O 模型,基于 “通道(Channel)” 和 “缓冲区(Buffer)” 实现,核心是 “非阻塞” 和 “多路复用”。线程发起 I/O 操作后无需阻塞等待,可立即返回处理其他任务;通过 “选择器(Selector)” 可以同时监控多个通道的 I/O 事件(如数据就绪、连接就绪),单个线程即可处理多个连接,大幅减少线程开销。
适用场景:高并发、I/O 操作频繁的场景(如服务器端网络通信、RPC 框架)。

AIO(Asynchronous I/O,异步 I/O):
JDK 1.7 引入,属于真正的异步 I/O 模型。线程发起 I/O 操作后会立即返回,由操作系统在后台完成实际的 I/O 处理,当操作完成后,操作系统会通知应用程序(通过回调或事件)并交付结果。整个过程中,应用线程无需阻塞或轮询,资源利用率更高。
适用场景:对响应速度要求高、I/O 操作耗时较长的场景(如大文件读写、高性能服务器)。

18.什么是 Channel?

在 Java NIO(非阻塞 I/O)中,Channel(通道) 是用于连接数据源(如文件、网络套接字)与程序的双向数据传输通道,类似于传统 BIO 中的流(Stream),但具有更强大的功能和特性。

与流(Stream)相比,Channel 有以下核心特点:
双向性:流是单向的(输入流只能读,输出流只能写),而 Channel 既可读也可写,无需分别创建输入和输出通道。
非阻塞支持:Channel 可与 Selector(选择器)配合实现非阻塞 I/O 操作,允许单线程监控多个通道的 I/O 事件(如数据就绪),大幅提升高并发场景下的性能。
面向缓冲区:Channel 必须与 Buffer(缓冲区)配合使用,所有数据的读写都通过缓冲区进行(读操作将数据从 Channel 写入 Buffer,写操作将数据从 Buffer 写入 Channel),而非直接操作数据。

常见的 Channel 实现类包括:
FileChannel:用于文件的读写操作。
SocketChannel/ServerSocketChannel:用于 TCP 网络通信,分别对应客户端和服务器端。
DatagramChannel:用于 UDP 协议的数据报传输。

Pipe.SinkChannel/Pipe.SourceChannel:用于线程间通过管道传输数据。
Channel 的出现是 Java NIO 区别于传统 BIO 的核心标志之一,它通过双向性、非阻塞支持和缓冲区导向的设计,为高并发 I/O 场景提供了更高效、灵活的处理方式。

19.什么是 Selector

在 Java NIO 中,Selector(选择器) 是实现非阻塞 I/O 的核心组件,它允许单个线程同时监控多个 Channel(通道)的 I/O 事件(如连接就绪、数据可读、数据可写等),从而实现 “单线程管理多通道” 的高效并发模型。

Selector 的核心作用是多路复用:通过将多个 Channel 注册到 Selector 上,线程无需为每个 Channel 单独阻塞等待,而是通过 Selector 轮询所有注册的 Channel,仅处理那些发生了特定 I/O 事件的 Channel。这种方式大幅减少了线程数量和上下文切换开销,特别适合高并发场景(如服务器需要处理成千上万的网络连接)。
工作原理:

注册 Channel:需将 Channel 配置为非阻塞模式(configureBlocking(false)),再通过 register() 方法注册到 Selector 上,并指定关注的事件(如 SelectionKey.OP_READ 表示关注读事件)。

轮询事件:线程调用 Selector 的 select() 方法(阻塞等待)或 selectNow() 方法(非阻塞),获取所有发生了注册事件的 Channel 对应的 SelectionKey(事件密钥)。

处理事件:通过 SelectionKey 遍历就绪的 Channel,执行相应的 I/O 操作(如读取数据),操作完成后可再次注册事件,循环往复。
关键特性:

事件驱动:仅当 Channel 有实际 I/O 事件发生时才会被处理,避免了无效等待。
单线程高效管理多连接:无需为每个连接创建线程,降低了系统资源消耗。

配合非阻塞 Channel:必须与非阻塞模式的 Channel(如 SocketChannel、ServerSocketChannel)配合使用,文件通道(FileChannel 不支持非阻塞)无法注册到 Selector。

Selector 是 Java NIO 实现高并发网络编程的核心机制,也是 Netty 等高性能框架的底层基础之一,通过它可以用极少的线程处理大量并发连接,显著提升系统的吞吐量。

0 条评论

发布
问题