Java全栈面试题汇总目录

目录

关键字

this关键字的用法?

super关键字的用法?

this与super核心区别对比?

为何super(参数列表)必须是子类构造方法的第一行可执行代码,且不能和this(参数列表)同时出现?

final有什么用?

final,finally,finalize区别?

switch是否能作用在byte上,是否能作用在long上,是否能作用在String上?

static关键字作用?

volatile关键字的作用?

transient关键字的作用?

native关键字的作用?

instanceof关键字的作用?

哪些关键字不能和abstract共用?

Java的访问修饰符关键字,与其的作用和区别?

面向对象

面向对象和面向过程的区别?

面向对象基本原则是什么?

面向对象的特征有哪些方面?

Java多态的实现方式?

重载和重写的区别?

Java对于重写的要求是什么?

为何Java对于重写要两同、两小、一大?

抽象类和接口的对比?

何时选择接口,何时选择抽象类?

普通类和抽象类有哪些区别?

类与对象

对象实例与对象引用有何不同?

成员变量与局部变量的区别有哪些?

在Java中定义一个不做事且没有参数的构造方法的作用?

构造方法有哪些特性?

静态变量和实例变量区别?

在静态方法内调用非静态变量为什么是非法的?

方法的返回值及其作用是什么?

什么是内部类?

内部类的分类有哪些?

局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?

构造器(constructor)是否可被重写(override)?

对象的相等与指向他们的引用相等,两者有什么不同?

==和equals的区别是什么?

hashCode()与equals()的关系是什么?

当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果那么这里到底是值传递还是引用传递?

为什么Java中只有值传递?

JDK

JDK中常用的包有哪些?

java.*和javax.*的区别有什么区别?

什么是字符串常量池?

String有哪些特性?

String真的是不可变的吗?

String str="i"与String str = new String("i")一样吗?

在使用HashMap的时候,用String做key有什么好处?

String和StringBuffer、StringBuilder的区别是什么?

自动装箱与拆箱是什么?

IO流&反射&序列化

java中IO流分为几种?

BIO,NIO,AIO有什么区别?

序列化和反序列化是什么?为什么要实现Serializable?

什么是反射机制?

反射机制的应用场景有哪些?

Java获取反射有几种方法?

深拷贝和浅拷贝有什么区别?

如何实现深拷贝?


关键字

this关键字的用法?

this关键字的核心含义是当前对象的引用(即“当前正在操作的这个对象本身”)

this的核心用法

1.区分成员变量与局部变量

当方法参数/局部变量名与成员变量名相同时,用this.成员变量名明确指代成员变量。

2.调用本类的其他构造方法

在构造方法中用this(参数列表)调用本类其他构造方法,必须放在第一行

3.调用本类的成员方法

用this.方法名()调用当前对象的其他成员方法(this可省略,写出来更清晰)。

4.作为参数/返回值传递当前对象

this可直接代表当前对象,作为参数传给其他方法,或作为返回值(常用于链式调用)。

关键注意事项

  1. this只能在非静态方法/构造方法中使用(静态方法属于类,无“当前对象”)。
  2. this(参数)只能在构造方法中用,且必须是第一行代码。
  3. 禁止构造方法通过this(...)互相递归调用(编译报错)。

super关键字的用法?

super是Java中处理继承关系的核心关键字,核心含义是当前对象中持有的「直接父类对象的引用」。和this(代表当前对象本身)对应,super专门用于在子类中访问父类的成员,解决继承场景下的成员重名、方法重写、父类初始化等核心问题。

一、super核心用法

1.调用父类的构造方法(基础必用场景)

子类实例化时,必须先完成父类对象的初始化。Java会在子类构造方法中隐式插入super()(调用父类无参构造);如果父类没有无参构造,或需要调用父类有参构造,就必须显式使用super(参数列表)。

  • 硬性规则:super(参数)必须写在子类构造方法的第一行,且不能和this(参数)同时使用(二者都要求在第一行)。
  • 补充:如果父类只定义了有参构造、没有无参构造,子类不显式调用super(参数)会直接编译报错(默认的super()找不到对应的构造方法)。

2.访问父类的同名成员变量

当子类定义了和父类同名的成员变量时,this.变量名访问的是子类自身的变量,super.变量名可以明确访问父类中定义的成员变量。

  • 权限限制:无法访问父类的private私有变量,只能访问父类public、protected、同包下默认权限的变量。

3.调用父类被重写的成员方法(最常用场景)

当子类重写(@Override)了父类的方法后,默认调用的是子类重写后的方法。如果需要调用父类中原本的方法实现,必须使用super.方法名(参数)。

  • 典型场景:在重写的方法中复用父类的核心逻辑,再扩展子类专属的业务逻辑。

二、关键注意事项

  1. 使用位置限制:super只能在非静态的成员方法、构造方法中使用,静态方法中无法使用(静态方法属于类,没有实例对象,也就不存在父类对象的引用)。
  2. 构造方法调用规则:super(参数列表)必须是子类构造方法的第一行可执行代码,且不能和this(参数列表)同时出现。
  3. 访问权限限制:super无法访问父类的private私有成员(变量/方法),只能访问父类中可被子类继承的成员。
  4. 继承层级查找:super优先访问直接父类的成员,如果直接父类没有,会继续向上追溯到祖先类,直到Object类。

this与super核心区别对比?

特性 this关键字 super关键字
核心含义 当前对象本身的引用 当前对象中父类对象的引用
构造方法调用 调用本类的其他构造方法,必须在构造方法第一行

调用父类的构造方法,必须在子类构造方法第一行

成员访问 优先访问本类的成员,本类没有则向上查找父类 直接访问父类的成员,跳过本类的同名成员
静态上下文 不能在静态方法中使用 不能在静态方法中使用
继承依赖 不依赖继承,普通类中即可使用 必须依赖继承,只能在子类中使用

为何super(参数列表)必须是子类构造方法的第一行可执行代码,且不能和this(参数列表)同时出现?

这两个规则的本质,是Java为了保证对象实例化过程中,父类成员的完整、唯一、安全初始化,从语法层面设定的强制规范,底层完全基于Java的继承模型和对象实例化机制。下面分两部分拆解根本原因,结合底层逻辑和反例讲清楚。

一、为什么super(参数列表)必须是子类构造方法的第一行可执行代码?

底层核心前提:Java的继承内存模型与初始化规则

首先要纠正一个常见误区:super()不会创建新的父类对象

当你new子类()时,JVM全程只创建一个子类实例对象,该对象的内存布局中,天然包含了「父类定义的所有实例成员变量」+「子类定义的实例成员变量」。

子类继承了父类的所有非私有成员,子类的构造方法、实例方法,随时可能访问这些父类成员。因此Java有一条不可打破的核心规则:

必须先完成父类成员的初始化,才能执行子类自身的初始化逻辑。

如果父类成员还没初始化,子类代码就访问它,会拿到JVM给的默认初始值(0/null/false),而非父类构造方法中赋值的正确数据,直接导致逻辑错误、数据异常,甚至安全漏洞。

编译器的隐式兜底

哪怕你手动不写super(),编译器也会自动在子类构造方法的第一行插入super()(调用父类无参构造),就是为了强制保证父类初始化永远优先于子类。

二、为什么super(参数列表)不能和this(参数列表)同时出现?

1.先明确两个构造调用的核心语义

  • this(参数列表):委托调用本类的其他重载构造方法,核心目的是复用本类的初始化逻辑。
  • super(参数列表):直接调用父类的构造方法,核心目的是完成父类成员的初始化。

2.第一层原因:语法层面的天然互斥

this(参数列表)和super(参数列表)都强制要求必须是构造方法的第一条可执行语句。同一个方法中,不可能存在两个“第一条语句”,仅从语法规则上,二者就无法共存。

3.第二层原因:语义层面的根本冲突(核心)

Java有一条不可打破的铁则:

一个对象的实例化过程中,父类的构造方法有且仅能被执行一次。

父类成员是子类对象的一部分,重复初始化会导致数据被覆盖、状态混乱,破坏对象的唯一性和一致性。

而this(参数列表)的委托机制,最终一定会追溯到父类的构造方法

任何构造方法,要么显式/隐式调用super(),要么通过this()委托给本类的其他构造方法;而最终被委托的那个“终点构造方法”,一定会调用super()完成父类初始化。

如果允许二者同时使用,必然会导致父类构造方法被多次调用,直接违反上述铁则。

4.补充:杜绝循环调用的风险

如果允许二者共存,还可能出现构造方法的递归循环调用(比如两个构造方法互相用this()委托,同时又调用super()),导致实例化陷入死循环,Java从语法层面直接杜绝了这种风险。

总结

这两个规则的设计初衷,是Java作为强类型、安全优先的语言,从语法层面强制保证:

  1. 所有子类对象,一定是父类成员先完成初始化,再执行子类逻辑,从根源避免未初始化访问
  2. 一个对象的实例化过程中,父类构造有且仅执行一次,保证对象状态的唯一性和一致性

final有什么用?

final是Java里的最终、不可变关键字,核心作用就是:限制修改、限制继承、限制重写

它可以修饰三类东西:变量、方法、类。

1.final修饰变量

含义:变量只能被赋值一次,赋值后不能再改

(1)修饰成员变量

  • 必须在声明时构造方法里赋值
  • 赋值后不能再修改
class A {
    final int MAX = 100;   // 直接赋值
    final int MIN;

    public A() {
        MIN = 0;          // 构造方法赋值
    }
}

(2)修饰局部变量

  • 可以先声明,后赋值
  • 只能赋值一次
public void test() {
    final int a;
    a = 10;   // 第一次赋值 ✔
    a = 20;   // 第二次赋值 ❌ 编译报错
}

(3)修饰引用类型变量

注意:引用地址不能变,但对象内容可以变

final List<String> list = new ArrayList<>();
list.add("a");   // ✔ 可以修改对象内容
list = new ArrayList<>();  // ❌ 不能改引用指向

2.final修饰方法

含义:方法不能被子类重写(Override)

class Father {
    public final void say() {
        System.out.println("不能被重写");
    }
}

class Son extends Father {
    // 重写会报错
    // public void say() {} ❌
}

用途:

  • 不想让子类破坏核心逻辑
  • 提高效率(JVM可以优化final方法)

3.final修饰类

含义:类不能被继承

final class A {
}

// class B extends A {} ❌ 报错

Java里很多常用类都是final:

  • String
  • Integer、Double等包装类
  • Math

目的:保证类的行为不可被篡改、安全、不可变

4.总结一句话

  • final变量:只能赋值一次
  • final方法:不能被子类重写
  • final:不能被继承

一句话概括:final=最终的,不可变的,不可扩展的

final,finally,finalize区别?

  • final:关键字,用来限制类、方法、变量,不让改、不让继承、不让重写。
  • finally:关键字,用在try-catch里,无论是否异常,一定执行
  • finalize():Object里的方法,GC回收对象前可能调用,现在基本废弃。

1.final(修饰符,静态限制)

作用:“最终的、不可变的”

三种用法:

修饰变量:变量变成常量,赋值后不能再改。

修饰方法:方法不能被子类重写。

修饰类:类不能被继承(比如String、Integer都是final)。

2.finally(异常处理,保证执行)

配合try-catch使用,无论是否抛异常、是否return,finally块一定执行

典型用途:

  • 关闭文件流
  • 关闭数据库连接
  • 释放锁

3.finalize()(对象回收方法,已废弃)

  • Object类里的一个方法
  • JVM在垃圾回收GC销毁对象前,可能会调用它
  • 调用时间不确定,甚至可能永远不调用
  • 不能依赖它释放资源、关闭连接

Java9已经标记为废弃(Deprecated),不建议使用。

快速对比表

关键字/方法 位置 作用 执行时机

final

修饰类/方法/变量

不可变、不可继承

编译期限制

finally

try-catch结构

保证代码一定执行

异常处理结束时

finalize()

Object成员方法

对象回收前清理

GC不确定时机

switch是否能作用在byte上,是否能作用在long上,是否能作用在String上?

1.switch支持的类型

  • byte/Byte
  • short/Short
  • int/Integer
  • char/Character
  • enum枚举
  • String(Java7+新增)

2.为什么long不行?

  • switch底层设计上只支持32位及以下整数类型
  • long是64位,不在支持范围内
  • 编译直接报错

3.String什么时候可以?

  • JDK1.7及以上才支持
  • 底层是把String转成hashCode+equals实现的

static关键字作用?

static表静态,核心作用是:让成员属于类本身,而不属于某个对象,可以直接通过类名访问,不需要new对象。

1.修饰成员变量(静态变量/类变量)

  • 属于,所有对象共享同一份
  • 随着类加载而初始化,只初始化一次
  • 访问:类名.变量名(推荐),也可以对象访问
public class User {
    static int count;  // 静态变量
}

// 使用
User.count = 10;

2.修饰成员方法(静态方法/类方法)

  • 属于,可直接通过类名调用
  • 不能访问非静态成员(变量/方法)
  • 不能使用this、super
public static void print(){
    // this.name × 报错
}

3.修饰代码块(静态代码块)

  • 类加载时只执行一次,优先于构造方法
  • 常用于初始化静态资源
static {
    // 初始化逻辑
}

4.修饰内部类(静态内部类)

  • 不依赖外部类对象,可直接创建
  • 不能访问外部类的非静态成员
class Outer{
    static class Inner{}
}

// 使用
Outer.Inner inner = new Outer.Inner();

5.静态导包(import static)

  • 直接导入某个类的静态成员,使用时不用写类名
import static java.lang.Math.PI;

volatile关键字的作用?

volatile是JVM提供的轻量级同步机制,核心作用:

  1. 保证可见性
  2. 禁止指令重排序
  3. 不保证原子性

1.保证可见性(Visibility)

  • 一个线程修改了volatile变量,会立即刷新回主内存
  • 其他线程会立即读到最新值,不会使用工作内存中的旧缓存
  • 解决:多线程下变量不可见问题

2.禁止指令重排序(Prevent Instruction Reordering)

  • 编译器/CPU为优化性能会乱序执行指令
  • volatile会加内存屏障,保证代码执行顺序不变
  • 典型场景:DCL单例模式必须加volatile,防止半初始化对象

3.不保证原子性(Atomicity)

  • 像i++这种操作,本质是:读→改→写三步
  • volatile只保证每一步可见,不保证整体原子性
  • 多线程下仍会线程不安全,需要synchronized或AtomicInteger

transient关键字的作用?

transient关键字的作用只有一个:阻止被修饰的成员变量被序列化

核心要点

1.只能修饰成员变量,不能修饰方法、类、局部变量

2.被transient修饰的变量,在对象序列化(ObjectOutputStream)时会被忽略

3.反序列化后,该变量会被赋予默认值

  • 引用类型→null
  • 基本类型→0、false等

4.static变量也不会被序列化,和transient效果类似,但语义不同

简单示例

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    // 密码不希望被序列化保存
    private transient String password;
}

序列化后再读取,password会变成 null。

常见使用场景

  • 敏感信息:密码、密钥、token
  • 运行时临时数据:没必要持久化的字段
  • 不可序列化的对象引用(避免序列化报错)

其他

  • static+transient:static本身就不参与序列化,加不加transient结果一样
  • transient只影响Java原生序列化,对JSON、hessian等其他序列化框架不一定生效

native关键字的作用?

native关键字用来声明本地方法,表示这个方法的实现不是由Java语言编写,而是由C/C++实现,并通过JNI(Java Native Interface)调用。

核心要点

1.只做声明,不提供方法体

  • 只有方法签名,没有实现代码
  • 类似抽象方法,但关键字是native
public native void sleep(long millis);

2.作用

  • 调用操作系统底层功能、硬件相关操作
  • 执行对性能要求极高的代码
  • 复用已有的C/C++库

3.运行机制

  • JVM通过JNI加载对应的.so/.dll/.dylib本地库
  • 方法执行时会从Java栈切换到本地方法栈

4.常见例子

  • Object.hashCode()
  • Thread.sleep()
  • System.currentTimeMillis()
  • 很多Unsafe类方法

其他

  • native方法不属于Java字节码范畴,不受JVM内存管理、GC直接控制
  • 本地方法执行时,线程状态为Native
  • 大量使用native会降低跨平台性(Java一次编译到处运行的优势会丢失)
  • 本地代码出现问题容易导致JVM崩溃,而非普通异常

instanceof关键字的作用?

instanceof是Java中的类型比较运算符,作用是:判断一个对象是否是某个类、子类或接口实现类的实例,返回boolean结果

核心用法

对象 instanceof 类/接口
  • 结果为true:对象是该类型本身、子类、实现类的实例
  • 结果为false:不是该类型,或对象为null

常见使用场景

  1. 向下转型前做安全判断,避免ClassCastException
  2. 方法重载、多态场景下区分对象类型

其他

  1. null instanceof任何类型都返回false
  2. 基本数据类型不能用instanceof
  3. Java14+支持模式匹配,可直接转型
  4. 与Class.isInstance()功能类似,但instanceof是关键字,后者是方法

哪些关键字不能和abstract共用?

一共5个:final、private、static、native、strictfp

1.final(绝对不能共用)

  • abstract:方法必须被重写、类必须被继承
  • final:类不能被继承、方法不能被重写语义完全矛盾

2.private(绝对不能共用)

  • abstract方法需要子类重写
  • private方法子类根本看不见,无法重写矛盾,编译报错

3.static(绝对不能共用)

  • static方法属于类,可直接调用,不需要重写
  • abstrac方法没有实现,必须重写矛盾,编译报错

4.native(语法层面不能共用)

  • native方法有C/C++实现体
  • abstract方法没有方法体语法冲突

5.strictfp

strictfp修饰的方法必须有实现,abstract没有实现,不能共用。

Java的访问修饰符关键字,与其的作用和区别?

Java有4种访问权限,其中关键字3个public、protected、private还有一种是default(缺省,不写关键字)

1.public(公共)

  • 作用:任何地方都能访问
  • 范围:同类、同包、不同包子类、不同包无关类

2.protected(受保护)

  • 作用:主要给子类使用
  • 范围:同类、同包、不同包子类
  • 不同包的非子类不能访问

3.default(默认/包私有)

  • 作用:包级访问
  • 范围:同类、同包
  • 不同包(无论是不是子类)都不能访问

4.private(私有)

  • 作用:仅本类内部使用,对外完全隐藏
  • 范围:只有当前类
  • 同包、子类、外部类都不能访问
修饰符 同类 同包 不同包子类 不同包非子类
private
default
protected
public

面向对象

面向对象和面向过程的区别?

面向过程是「怎么做」,面向对象是「谁来做」。

1.核心思想

  • 面向过程(POP)过程/步骤为核心,把问题拆成一系列函数/步骤,一步步执行。关注:怎么实现流程
  • 面向对象(OOP)对象为核心,把数据和操作封装成对象,通过对象之间的交互解决问题。关注:有哪些实体、它们能做什么

2.关注点不同

  • 面向过程:功能/步骤
  • 面向对象:数据/实体

3.封装性

  • 面向过程:数据和方法分离,容易全局变量混乱
  • 面向对象:属性+方法封装在对象内,对外隐藏实现

4.扩展性、维护性

  • 面向过程:修改一处容易牵一发而动全身,难扩展、难维护
  • 面向对象:通过继承、多态、封装,易于扩展、复用、维护

5.复用方式

  • 面向过程:函数复用
  • 面向对象:类、对象、继承、接口复用

6.适合场景

  • 面向过程:简单、逻辑固定、小型程序(如算法、小脚本)
  • 面向对象:复杂项目、大型系统、需求常变、多人协作

7.总结

  • 面向过程:自顶向下、分步执行,侧重步骤实现,简单高效但扩展性差。
  • 面向对象:封装、继承、多态,侧重对象建模,耦合低、易扩展、适合大型项目。

面向对象基本原则是什么?

1.单一职责原则(SRP)

  • 一个类只负责一项职责
  • 一个类只因为一个原因而修改
  • 好处:类更简洁、易维护、耦合低

2.开闭原则(OCP)

  • 对扩展开放,对修改关闭
  • 新增功能用扩展实现,不修改原有代码
  • 面向接口/抽象编程,方便扩展

3.里氏替换原则(LSP)

  • 子类必须能完全替换父类,程序逻辑不变
  • 子类不能破坏父类原有功能、约束
  • 是继承复用的基础

4.接口隔离原则(ISP)

  • 建立单一、小粒度接口
  • 客户端不依赖不需要的接口
  • 避免臃肿接口,减少耦合

5.依赖倒置原则(DIP)

  • 高层模块不依赖低层模块,都依赖抽象
  • 抽象不依赖细节,细节依赖抽象
  • 面向接口编程,而非面向实现

6.迪米特法则(最少知道原则)

  • 一个对象只和直接朋友通信
  • 不和陌生人说话,减少类之间依赖
  • 降低耦合,提高独立性

面向对象的特征有哪些方面?

1.封装(Encapsulation)

  • 核心:把属性和方法包装在类中,隐藏内部实现,对外只暴露接口。
  • 手段
    • 访问修饰符:private、protected、public
    • 提供getter/setter方法访问私有变量
  • 好处
    • 数据安全,防止外部随意修改
    • 降低耦合,便于维护

2.继承(Inheritance)

  • 核心:子类继承父类,复用父类属性和方法,并可扩展。
  • 关键字:extends(类)、implements(接口)
  • 特点
    • Java只支持单继承,一个类只能有一个直接父类
    • 可以多层继承
  • 好处:代码复用,体现is-a关系

3.多态(Polymorphism)

  • 核心:同一行为,在不同对象上表现出不同形态。
  • 前提
    1. 继承/实现关系
    2. 方法重写(override)
    3. 父类引用指向子类对象
  • 体现:编译看左边,运行看右边
  • 好处:提高扩展性和灵活性

4.抽象(Abstract)

  • 抽取事物共性特征,忽略无关细节
  • 体现:抽象类、接口
  • 作用:规范行为,不关心具体实现

Java多态的实现方式?

Java多态主要分为编译时多态运行时多态

一、编译时多态(静态多态)

实现方式:方法重载(Overload)

  • 同一个类中,方法名相同,参数列表不同(个数、类型、顺序不同)
  • 编译器在编译阶段就确定调用哪个方法
  • 与返回值无关
public void add(int a, int b)
public void add(double a, double b)

二、运行时多态(动态多态)

实现方式:方法重写(Override)满足三个前提:

  • 继承/实现关系
  • 子类重写父类方法
  • 父类引用指向子类对象(向上转型)
Animal animal = new Cat();
animal.eat(); // 执行 Cat 的 eat,运行时才确定

三、Java中多态的具体体现形式

  1. 普通类继承+方法重写
  2. 抽象类继承+抽象方法实现
  3. 接口实现+接口方法重写

这三种本质都是运行时多态

重载和重写的区别?

特性 重写(Overriding) 重载(Overloading)
多态类型 运行时多态(真正的多态) 编译时多态(静态绑定,有争议)
绑定时机 运行时(Run-time) 编译时(Compile-time)
核心 子类改变父类行为 参数列表不同
继承要求 必须在父子类或接口与实现类中 通常在同一类中(也可在子类重载父类方法)
典型例子

List list = new ArrayList();

list.add()调用的是ArrayList的方法

System.out.println(1);

System.out.println(“a”);

Java对于重写的要求是什么?

遵循口诀:两同、两小、一大

1.两同(必须相同)

  1. 方法名完全相同
  2. 参数列表完全相同(个数、类型、顺序)

2.两小(子类更小/更严格)

1.返回值类型更小(协变返回)

  • 基本类型:必须完全一样
  • 引用类型:子类返回值可以是父类返回值的子类

2.抛出异常更小

  • 子类抛出的受检异常不能比父类更大、更多
  • 可以不抛、可以抛子类异常,但不能抛父类没有的新异常

3.一大(权限更大)

  • 子类方法的访问权限不能比父类更严格
  • 父类public→子类只能public
  • 父类protected→子类可以protected/public
  • 父类default→子类可以default/protected/public
  • 父类private:不能被重写(看不见)

额外重要规则

  • static方法不能被重写,只能被隐藏
  • final方法不能被重写
  • 构造方法不能被重写
  • 重写是针对实例方法,和变量无关(变量没有重写,只有隐藏)

为何Java对于重写要两同、两小、一大?

为了保证多态安全,让父类引用能无缝替换子类对象(里氏替换原则)。

里氏替换原则的核心是:所有使用父类的地方,都可以透明地替换为子类对象,程序行为不会出错

1.两同:方法名相同、参数列表相同

  • 多态调用的基础:当通过父类引用调用方法时,编译器在编译阶段只检查父类中是否存在该方法签名(方法名+参数列表)。运行时,JVM会根据实际对象类型找到对应的方法。如果子类的方法签名与父类不同,那它就不是重写,而是定义了一个全新的方法(重载或新方法),多态就无法生效。

  • 避免歧义:如果允许参数列表不同,那么父类引用在调用时,编译器就无法确定应该匹配哪个方法,会导致混乱。

2.两小:返回类型更小或相等、抛出的异常更小或相等

(1)返回类型更小或相等(协变返回类型)

  • 里氏替换原则:子类对象必须能够在不改变程序正确性的前提下替换父类对象。如果父类方法返回Animal,子类方法返回Dog(Dog是Animal的子类),那么通过父类引用调用该方法时,返回的对象依然可以安全地赋值给Animal类型的变量,不会破坏类型安全。

  • 允许更具体的返回:这样设计让子类可以返回更精确的类型,增强代码的表达力,同时保持兼容性。

如果允许返回父类或更宽泛的类型,那么调用者如果期待父类返回的特定子类功能,就可能出现类型转换异常。

(2)抛出的异常更小或相等

  • 异常处理契约:当通过父类引用调用方法时,调用者的try-catch或throws声明是基于父类方法抛出的异常来处理的。如果子类方法抛出一个父类未声明的检查型异常,那么调用者没有对应的处理代码,程序就会编译出错,破坏了原有的异常处理逻辑。

  • 细化异常:允许子类抛出更具体的异常(子类异常)是安全的,因为调用者已经捕获了父类异常,自然也能捕获其子类异常(向上转型捕获)。

3.一大:访问权限更大或相等

  • 多态的可访问性:父类引用可能来自不同的包或模块,其访问权限决定了调用者能否访问该方法。如果子类将方法从public改为protected或private,那么通过父类引用调用时,虽然实际对象是子类,但编译时检查是通过父类引用的类型进行的,如果子类降低了访问权限,运行时 JVM会因访问权限不足而抛出IllegalAccessError或编译失败。这破坏了“父类引用可以透明地调用子类方法”的多态特性。

  • 继承的承诺:父类公开的方法意味着它承诺所有子类都提供该功能,子类不能削弱这个承诺,否则会违反里氏替换原则。

抽象类和接口的对比?

相同点

  1. 不能被实例化(不能new)
  2. 都可以包含抽象方法
  3. 都可以被继承/实现,需要子类/实现类提供具体实现
  4. 都可以作为引用类型,用于多态

不同点

1.继承方式

  • 抽象类:用extends继承,Java只能单继承
  • 接口:用implements实现,可以多实现

2.构造方法

  • 抽象类:可以有构造方法
  • 接口:不能有构造方法

3.成员变量

  • 抽象类:可以有任意变量(实例变量、静态变量)
  • 接口:默认public static final常量,不能有普通成员变量

4.方法

  • 抽象类:可以有抽象方法、普通方法、静态方法
  • 接口(Java8+):抽象方法默认public abstract,可以有default方法、static方法

5.访问修饰符

  • 抽象类:方法可以用public/protected/default
  • 接口:方法默认public,不能用private/protected

6.设计意图

  • 抽象类:模板设计,抽取子类公共属性和行为
  • 接口:规范设计,定义能力、行为标准,不关心实现类是谁

何时选择接口,何时选择抽象类?

核心原则

  • 要定义行为规范、能力扩展→用接口
  • 要抽取公共代码、复用属性和通用逻辑→用抽象类

优先使用接口(Interface)的场景

1.需要多实现/多能力扩展

Java类只能单继承,但能实现多个接口,适合给不同类赋予同一能力。

2.只约定“能做什么”,不关心“怎么做”

比如:Serializable、Cloneable、Runnable、Comparable。

3.解耦、面向接口编程

高层依赖接口,不依赖具体实现,符合依赖倒置原则

4.跨类、跨模块的统一规范

结论:定义能力、规范、契约→用接口

必须使用抽象类(Abstract Class)的场景

1.多个子类有共同属性(成员变量)通用方法实现

抽象类可以封装成员变量+通用逻辑,子类直接复用。

2.需要使用构造方法做统一初始化

接口没有构造方法,抽象类可以。

3.要控制继承关系(is-a关系)

比如:Dog extends Animal,明确是一种从属关系。

4.需要使用非public权限、普通实例变量

结论:抽取共性、复用代码、模板设计→用抽象类

总结

  • 有属性、有实现、要复用→抽象类
  • 纯规范、要扩展、多实现→接口
  • 优先接口,再用抽象类做默认实现
  • 接口定义行为,抽象类做骨架实现

普通类和抽象类有哪些区别?

核心

  • 普通类:可以直接实例化,不能包含抽象方法
  • 抽象类:不能实例化,可以包含抽象方法

对比

1.能否被实例化(最根本区别)

  • 普通类:可以直接new创建对象
  • 抽象类:不能new,只能被继承,由子类实例化

2.能否包含抽象方法

  • 普通类:不能有抽象方法,所有方法必须有实现
  • 抽象类:可以包含抽象方法,也可以包含普通方法

3.关键字

  • 普通类:直接class
  • 抽象类:必须加abstract class

4.继承后的要求

  • 普通类继承普通类:直接继承,不需要强制重写任何方法
  • 普通类继承抽象类:必须重写所有抽象方法,否则编译报错

5.设计目的

  • 普通类:描述具体对象,直接使用
  • 抽象类:抽取共性、作为模板,约束子类必须实现某些方法

总结

  1. 抽象类用abstract修饰,普通类不用
  2. 抽象类不能实例化,普通类可以
  3. 抽象类可以有抽象方法,普通类不可以
  4. 子类继承抽象类必须实现抽象方法,继承普通类则不需要

类与对象

对象实例与对象引用有何不同?

对象实例是内存里真实存在的对象本身;对象引用是指向这个实例的“变量名/遥控器”。

本质区别

  • 对象实例(对象本身)
    • 通过new在堆内存中创建的真实数据
    • 包含属性、状态,是实际存在的实例
  • 对象引用
    • 存放在栈内存中的变量
    • 里面保存的是堆中对象的内存地址
    • 本身不是对象,只是一个“指向地址的变量”

代码举例

// person 是对象引用(在栈里)
// new Person() 是对象实例(在堆里)
Person person = new Person();
  • person:引用变量,存地址
  • new Person():真实对象实例,存数据

关键区别点

1.存储位置

  • 引用:栈内存
  • 实例:堆内存

2.数量关系

  • 一个实例可以被多个引用同时指向
  • 一个引用同一时间只能指向一个实例(或null)

3.生命周期

  • 引用随方法出栈就销毁
  • 实例由GC回收,没有引用指向时才会被清理

4.赋值含义

Person a = new Person();
Person b = a;

这里是把引用赋值,a和b指向同一个对象实例,不是复制对象。

总结

对象实例是堆中的真实对象;对象引用是栈中指向该对象的地址变量。

引用是绳子,实例是气球。

一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球)

一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)

成员变量与局部变量的区别有哪些?

对比项 成员变量(Member Variable) 局部变量(Local Variable)
定义位置 类中、方法外 方法内/语句块内/方法参数
作用域 整个类有效 仅当前方法/代码块内有效
初始值 有默认初始值,无需手动赋值 无默认值,必须显式初始化才能使用
内存位置 堆内存(对象中) 栈内存
生命周期 随对象创建而存在,对象回收时消失 方法/代码块执行时创建,执行完立即销毁
修饰符 可使用:public、private、protected、static、final等 只能使用:final
别称 全局变量(广义说法) 局部变量

在Java中定义一个不做事且没有参数的构造方法的作用?

核心作用

  1. 保证子类在继承时,可以正常调用父类的无参构造
  2. 防止因为显式写了有参构造,导致默认无参构造消失,从而出现编译错误

详细解释

1.Java默认规则

  • 一个类如果没有写任何构造方法,编译器会自动生成一个默认的无参构造。
  • 但如果你显式写了有参构造,编译器就不再自动生成无参构造。

2.子类继承时的问题

  • 子类构造方法执行时,会默认先调用父类的无参构造(super())。
  • 如果父类没有无参构造,子类就会编译报错。

3.手动写一个空的无参构造的原因

  • 保留无参构造
  • 让子类可以正常继承、实例化
  • 让框架(Spring、MyBatis等)可以通过反射newInstance()创建对象

构造方法有哪些特性?

  1. 方法名必须与类名完全相同,大小写一致
  2. 没有返回值类型,连都void不能写
  3. 不能被static、final、abstract、native修饰
  4. 可以有方法重载(多个参数列表不同的构造方法)
  5. 可以使用访问修饰符(常用public/private)
  6. 子类构造会默认先调用父类无参构造super()
  7. 若没写任何构造,编译器会自动生成一个默认无参构造
  8. 若显式写了有参构造,默认无参构造会消失
  9. 主要作用:创建对象时初始化成员变量

静态变量和实例变量区别?

对比项 静态变量(static变量) 实例变量(非static变量)
修饰关键字 有static 无static
归属对象 属于 属于对象实例
创建时机 类加载时初始化

new对象时才初始化

内存位置 方法区/静态区 堆内存(对象内部)
内存数量 整个JVM中只有一份 每个对象各有一份
调用方式 类名.变量名/对象.变量名 只能通过对象.变量名
生命周期 随类加载存在,类卸载销毁 随对象创建,对象GC回收销毁
赋值特点 所有对象共享同一份值 每个对象独立,互不影响
初始化顺序 优先于实例变量、构造方法 在静态变量之后,构造方法之前

在静态方法内调用非静态变量为什么是非法的?

核心原因

静态方法属于类,在类加载时就存在;非静态变量属于对象,只有创建对象后才存在。在静态方法里直接访问非静态变量,就是“用早了、对象还不存在”,所以非法。

详细解释

1.生命周期不同

  • 静态方法:类加载时就初始化,不需要创建对象就能调用。
  • 非静态变量:属于实例,必须出new对象后才会在堆中分配内存。

2.调用时机矛盾

  • 当你调用静态方法时,完全可以没有任何对象存在,而非静态变量依赖对象。
  • 这时候JVM根本不知道你要访问哪个对象的变量,所以直接编译报错。

总结

因为静态成员隶属于类,随类加载而存在;实例成员隶属于对象,随对象创建而存在。静态方法执行时,对象可能尚未创建,无法确定访问哪个对象的非静态变量,因此编译器直接禁止这种跨生命周期的非法调用。

方法的返回值及其作用是什么?

方法的返回值

  • 方法执行完成后,返回给调用处的一个数据
  • 用return关键字带出
  • 方法声明时必须指定返回类型:void(无返回)/基本类型/引用类型

返回值的作用

  1. 把方法的执行结果交给调用方使用
  2. 提前结束方法执行(return;直接退出)
  3. 让方法只负责计算,不负责处理结果,提高复用性

什么是内部类?

定义在一个类内部的类,就叫内部类;包含它的类称为外部类。

内部类的作用

  1. 间接实现多继承(一个类可以继承多个内部类)
  2. 可以方便访问外部类的私有成员,外部类也能访问内部类私有成员
  3. 对外部隐藏实现,提高封装性
  4. 适合创建只在当前类内部使用的辅助类

常见4种内部类

  1. 成员内部类:类中方法外,无static
  2. 静态内部类:带static,属于外部类本身
  3. 局部内部类:定义在方法/代码块里
  4. 匿名内部类:没有类名,一次性使用(最常用)

内部类的分类有哪些?

内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类

1.成员内部类(普通内部类)

强制要求

  • 必须定义在类内部、方法外部
  • 不能定义static成员(静态变量、静态方法)
  • 必须依赖外部类对象才能创建

特点

  • static修饰
  • 持有外部类引用,可直接访问外部类所有成员(包括private
  • 创建必须通过外部类实例

优点

  • 可以直接访问外部类所有成员,简化调用
  • 封装性更强,内部类对外隐藏
  • 逻辑上属于外部类,结构更内聚

应用场景

  • 作为外部类的辅助类,只服务于当前外部类
  • 需要频繁访问外部类私有成员的场景
  • 逻辑上与外部类强绑定的功能模块

代码示例

class Outer {
    private int outerVar = 100;

    class Inner {
        void show() {
            System.out.println("外部类变量:" + outerVar);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.show();
    }
}

2.静态内部类

强制要求

  • 必须使用static修饰
  • 只能访问外部类的static成员,不能访问实例成员
  • 可以直接创建,不需要外部类对象

特点

  • 属于外部类本身,不持有外部对象引用
  • 作用和普通类类似,只是名字带外部类前缀

优点

  • 不持有外部引用,不会造成内存泄漏
  • 加载、创建效率高,不依赖外部对象
  • 减少类名冲突,组织结构更清晰

应用场景

  • 作为外部类的静态工具类
  • 只需要使用外部静态资源的场景
  • 希望独立存在、不依赖外部实例的类

代码示例

class Outer {
    private static int staticVar = 200;

    static class StaticInner {
        void show() {
            System.out.println("外部静态变量:" + staticVar);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Outer.StaticInner inner = new Outer.StaticInner();
        inner.show();
    }
}

3.局部内部类

强制要求

  • 必须定义在方法或代码块内部
  • 不能使用访问修饰符(public/private/protected
  • 不能使用static修饰
  • 只能访问方法中final或有效final的局部变量

特点

  • 作用域仅限于当前方法
  • 对外部完全隐藏,是方法内部的临时类

优点

  • 封装性极高,仅在方法内可见
  • 临时逻辑专用,不污染类结构
  • 可以访问外部类成员与方法内变量

应用场景

  • 方法内部临时需要封装复杂逻辑
  • 只在某个方法内使用一次的辅助类
  • 不想暴露给外部的局部功能

代码示例

class Outer {
    private int outerVar = 300;

    void method() {
        final int localVar = 400;

        class LocalInner {
            void show() {
                System.out.println("外部变量:" + outerVar);
                System.out.println("局部变量:" + localVar);
            }
        }

        LocalInner inner = new LocalInner();
        inner.show();
    }
}

public class Test {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.method();
    }
}

4.匿名内部类

强制要求

  • 必须继承一个父类或者实现一个接口,二者选其一
  • 没有类名,不能定义构造方法
  • 只能创建一个实例,一次性使用
  • 不能定义static成员(静态变量、静态方法)
  • 可以访问外部类成员,以及方法内final或有效final变量

特点

  • 语法格式:new 类/接口() { 重写方法 }
  • 本质是创建一个子类/实现类对象

优点

  • 代码简洁,省去单独定义实现类
  • 使用灵活,随用随定义
  • 适合一次性逻辑,避免类过多

应用场景

  • 快速实现接口、抽象类
  • 事件监听、按钮点击回调
  • 线程创建、定时任务
  • 简单的一次性逻辑封装

代码示例

interface MyInterface {
    void sayHello();
}

public class Test {
    public static void main(String[] args) {
        MyInterface obj = new MyInterface() {
            @Override
            public void sayHello() {
                System.out.println("Hello from 匿名内部类");
            }
        };
        obj.sayHello();
    }
}

常用场景(线程)

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程运行");
    }
}).start();

局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?

因为局部变量生命周期在方法栈,内部类对象生命周期在堆,两者生命周期不一致,Java 用复制变量副本的方式实现访问,为了保证副本和原变量值一致,必须限制为不可修改的final

详细解释

1.生命周期不一样

  • 局部变量:在里,方法执行完就销毁。
  • 内部类对象:在里,方法结束后可能还活着(被引用、延迟执行等)。

2.内部类用的是“副本”,不是原变量

  • 编译器会把局部变量复制一份给内部类使用。如果原变量能改,副本不会同步更新,就会出现数据不一致

3.强制final=禁止修改

  • 一旦赋值就不能变,副本和原值永远一致,保证安全。

Java8+的变化

Java8开始引入有效final(effectively final):

  • 变量没被final修饰,但只赋值一次,编译器自动当成final。
  • 只要赋值后不修改,就可以直接访问,不用手动写final。

总结

局部变量存于栈,方法结束即销毁;内部类对象存于堆,生命周期更长。

内部类实际访问的是变量副本,为防止原值与副本不一致,Java要求局部变量必须是final或有效 final,保证值不可变。

构造器(constructor)是否可被重写(override)?

构造器不能被重写(override)。

原因

1.重写的前提

重写是针对父类的方法,子类提供相同方法签名的实现。而构造器名必须和类名一致,子类构造器名和父类必然不同,根本不满足重写的语法条件。

2.生命周期与作用不同

构造器用于创建对象、初始化实例,不属于实例方法,不存在“继承后覆盖”的逻辑。

3.构造器可以被重载,但不能被重写

  • 同一个类中多个构造器→重载(overload)
  • 子类写自己的构造器→不是重写,只是子类自己的构造方法

对象的相等与指向他们的引用相等,两者有什么不同?

1.引用相等

  • 含义:两个引用变量指向堆中同一个对象
  • 判断方式:使用==比较
  • 本质:比较的是内存地址是否相同
  • 特点:引用相等→对象一定相等

示例

String s1 = new String("abc");
String s2 = s1;   // s2 和 s1 指向同一个对象

System.out.println(s1 == s2);  // true,引用相等

2.对象相等

  • 含义:两个引用指向不同对象,但对象内部的内容相同
  • 判断方式:使用equals()方法比较
  • 本质:比较的是对象的内容/状态
  • 特点:对象相等→引用不一定相等

示例

String s1 = new String("abc");
String s2 = new String("abc");

System.out.println(s1 == s2);        // false,引用不同
System.out.println(s1.equals(s2));   // true,对象内容相等

总结

  • ==比较地址,看是不是同一个对象(引用相等)
  • equals()比较内容,看对象数据是否一样(对象相等)

==和equals的区别是什么?

一、==的作用

  1. 比较基本数据类型:比较的是值是否相等
  2. 比较引用数据类型:比较的是两个引用指向的内存地址是否相同,即是否是同一个对象。

二、equals的作用

1.默认情况(Object类中的equals)

  • ==完全一样,比较内存地址

2.重写后(如String、Integer等)

  • 比较的是对象的内容是否相等,不再比较地址。
  • 像String、Integer、Date等类都默认重写了equals

三、核心区别总结

1.==

  • 既可以比较基本类型,也可以比较引用类型
  • 基本类型:比值;引用类型:比地址。

2.equals

  • 只能比较引用类型
  • 没重写:比地址;
  • 重写后:比内容。

四、经典例子(String)

String s1 = new String("hello");
String s2 = new String("hello");

System.out.println(s1 == s2);       // false,地址不同
System.out.println(s1.equals(s2));  // true,内容相同

hashCode()与equals()的关系是什么?

一、官方规定的必须遵守的规则

  1. 如果两个对象调用equals()结果为true→它们的hashCode()必须相等。
  2. 如果两个对象hashCode()相等→equals()不一定为true(哈希冲突)。
  3. 如果两个对象equals()为false→hashCode()尽量不相等(提高哈希表效率)

二、为什么要有这种关系?

主要为了HashMap、HashSet、Hashtable等集合能正常工作:

  1. 集合先通过hashCode快速定位到桶位置
  2. 再用equals逐个比较,判断是否重复。如果hashCode乱了,集合就会找不到元素、存重复值、删不掉数据

三、开发规范

  • 重写equals()时,必须同时重写hashCode()
  • 保证:参与equals比较的字段,也要参与hashCode计算
  • 否则存入HashSet/HashMap会出现逻辑错误

四、总结

hashCode是粗略定位,equals是精确判断;

相等对象必须同哈希,同哈希对象不一定相等。

当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果那么这里到底是值传递还是引用传递?

是值传递,Java语言里只有值传递这一种参数传递方式。

为什么是值传递?

1.传递的不是对象本身,而是“对象引用的副本”

  • 把对象传给方法时,JVM会复制一份引用地址,把这个副本传给方法。
  • 本质上传递的还是一个值(地址值),所以仍然是值传递

2.能修改属性≠引用传递

  • 因为引用副本和原引用指向同一个堆对象,所以通过副本操作对象,能修改属性。
  • 但如果在方法内把引用指向新对象外部原来的引用不会变

代码示例

class Person {
    int age = 10;
}

public class Test {
    public static void change(Person p) {
        // 修改对象属性:外部会变,因为操作的是同一个对象
        p.age = 20;
        
        // 让 p 指向新对象:外部完全不受影响
        // 证明传递的是副本,不是原引用本身
        p = new Person();
        p.age = 30;
    }

    public static void main(String[] args) {
        Person person = new Person();
        change(person);
        System.out.println(person.age); // 20,不是 30
    }
}

为什么Java中只有值传递?

1.明确两个定义

值传递(pass by value)

方法接收的是变量值的一份副本。修改副本,不会影响原来的变量

引用传递(pass by reference)

方法直接接收变量本身的引用/地址。方法内部修改,会直接改变外部原变量

2.Java到底怎么做的?

①基本类型

直接拷贝值的副本,方法内怎么改都不影响外面。

public static void change(int x) {
    x = 100;
}

int a = 10;
change(a);
// a 仍然是 10

典型的值传递。

②引用类型(对象)

拷贝的是引用地址的值的副本

  • 副本和原引用指向同一个对象
  • 所以能改对象属性
  • 不能改变原引用的指向
public static void setNull(Person p) {
    p = null; // 只改了副本的指向
}

Person person = new Person();
setNull(person);
// person 依然不为 null!

这说明:传的是副本,不是原引用本身→仍然是值传递

3.为什么说Java只有值传递?

因为:

  1. 不管基本类型还是引用类型,传递的都是副本
  2. 引用类型传递的是地址值的副本,不是对象本身,也不是原引用
  3. 方法永远不能修改外部变量本身,只能修改副本
  4. 真正的引用传递是:方法能直接替换外部变量的值,Java做不到

满足值传递的全部特征,不满足引用传递任何一条。

4.一句话终极总结

  • Java总是将变量的值复制一份传给方法。
  • 基本类型复制值,引用类型复制地址值。
  • 因为都是传“副本”,所以Java只有值传递。

JDK

JDK中常用的包有哪些?

1.java.lang

  • 核心基础类,默认自动导入,无需手动import
  • 包含:Object、String、Integer、Long、Double、Math、System、Thread、Exception等

2.java.util

  • 工具类与集合框架
  • 包含:List、Set、Map、ArrayList、LinkedList、HashMap、HashSet、Collections、Arrays、Date、Calendar等

3.java.io

  • 输入输出流,文件操作
  • 包含:File、InputStream、OutputStream、Reader、Writer、BufferedReader等

4.java.net

  • 网络编程相关
  • 包含:Socket、ServerSocket、URL、HttpURLConnection等

5.java.sql

  • 数据库操作JDBC
  • 包含:Connection、Statement、PreparedStatement、ResultSet等

6.java.math

  • 高精度数值计算
  • 包含:BigInteger、BigDecimal

7.java.tex

  • 格式化、日期格式化
  • 包含:SimpleDateFormat、DecimalFormat

8.java.awt、javax.swing

  • 图形界面GUI开发(现在较少用)

9.java.util.concurrent

  • 高并发、多线程工具包
  • 包含:ThreadPoolExecutor、CountDownLatch、CyclicBarrier、Lock等

java.*和javax.*的区别有什么区别?

1.起源与定位

  • java.*:Java核心标准包,是Java语言最基础、最稳定的API。
  • javax.*:最初是Java Extension(扩展包),后来很多扩展被纳入标准,但包名没改。

2.历史原因

  • 早期Java分两部分:
    • java.:核心API,必须实现,不能随意改动。
    • javax.:扩展API,可选实现,如Servlet、Swing等。
  • 后来很多扩展(如Swing、JMX、JAXB)正式成为标准,但为了兼容旧代码,包名依然保留 javax.没改成java.

3.现在的实际区别

  • 本质上已经没有严格界限,都是Java标准API。
  • 只是历史遗留命名:
    • 基础类→java.
    • 曾经是扩展、后来标准化→javax.

4.常见包举例

  • java.:java.lang、java.util、java.io、java.net、java.sql
  • javax.:javax.servlet、javax.swing、javax.annotation、javax.persistence

什么是字符串常量池?

字符串常量池是JVM专门用来存放字符串常量的一块内存区域,目的是共享字符串、节省内存、提高效率。它位于方法区(JDK7及之后移到了中)。

特点

  • 相同内容的字符串只存一份,多个引用可以共用同一个对象。
  • 使用String s = "abc";这种直接赋值方式时,JVM会先去常量池找是否已有"abc"
    • 有→直接返回池中对象的引用
    • 没有→创建并存入池中,再返回引用
  • 使用new String("abc")时,一定会在堆中新建对象,不会直接复用池对象。

两种创建方式的区别

①直接赋值(入池)

String s1 = "hello";
String s2 = "hello";
  • s1和s2指向常量池同一个对象
  • s1==s2true

②new创建(不入池)

String s3 = new String("hello");
String s4 = new String("hello");
  • 每次new都在堆中创建新对象
  • s3==s4false

String intern()方法

  • intern()作用:强制将字符串入池
  • 如果池中已有相同内容,返回池中的引用
  • 如果没有,就把当前字符串加入池并返回引用

总结

字符串常量池是JVM用来缓存共享字符串的内存区域,相同字符串只存一份,用双引号直接定义的字符串会自动入池,new String则不会。

String有哪些特性?

1.不可变性(最核心特性)

  • String对象一旦创建,内容就不能被修改
  • 底层实现:用final修饰的char[]数组存储(JDK9改为byte[])。
  • 所有看似修改的操作(substring、replace、concat等),都是返回新的String对象

2.常量池优化

  • 使用双引号直接创建的字符串,会进入字符串常量池
  • 相同内容的字符串,常量池中只存一份,实现共享、节省内存。

3.类被final修饰

  • String类被final修饰,不能被继承。

  • 目的:保证安全、防止被篡改、保证字符串唯一性。

4.equals和hashCode被重写

  • equals()比较的是字符串内容,不是地址。

  • hashCode()根据字符串内容计算,保证相同内容hashCode相同。

5.实现了多个序列化接口

  • 实现了Serializable、Comparable、CharSequence。

  • 支持序列化、排序、字符序列操作。

6.线程安全

  • 因为不可变,多线程环境下可以安全共享,无需同步

7.特殊的创建方式

  • String s = "abc";:先去常量池找,找不到再创建。

  • String s = new String("abc");:一定在堆上创建新对象。

String真的是不可变的吗?

1.String不可变但不代表引用不可以变

  • String对象本身不可变:内容一旦创建,就不能修改。
  • 引用变量可以重新赋值:这个变量可以指向别的字符串对象。
String str = "Hello";
str = str + " World";
System.out.println("str=" + str);

结果

str=Hello World

实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。

2.通过反射是可以修改所谓的“不可变”对象

//创建字符串"Hello World",并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World

结果:

s = Hello World
s = Hello_World

这种破坏不可变的行为不推荐、不规范、极容易引发bug(比如常量池缓存混乱),Java设计上依然把String视为不可变。

String str="i"与String str = new String("i")一样吗?

1.内存方式不同

  • String str = "i";

    • 字符串常量池里找"i"
    • 有就直接复用,没有就创建一个放入常量池
    • 最多创建 1 个对象
  • String str = new String("i");

    • 先在中创建一个新的String对象
    • 同时常量池中如果没有"i",也会创建一份
    • 至少创建1个,最多2个对象

2.==比较结果不同

String s1 = "i";
String s2 = "i";
String s3 = new String("i");
String s4 = new String("i");
  • s1==s2→true(指向常量池同一个对象)
  • s3==s4→false(两个不同堆对象)
  • s1==s3→false(一个在池,一个在堆)

3.性能与内存

  • ""方式:高效、省内存、复用性高
  • new String():浪费内存,不推荐使用

在使用HashMap的时候,用String做key有什么好处?

1.String不可变,保证hashCode一次性计算且不会变

  • String是不可变类,创建后内容不能改
  • 因此hashCode只会计算一次并缓存起来,后续使用极快
  • 如果key是可变对象,修改后hashCode会变,HashMap就会找不到数据、数据丢失、内存泄漏

2.已经重写了equals()和hashCode()

  • 标准、正确、无坑
  • 不会出现自己写类漏重写方法导致HashMap异常的问题
  • 比较逻辑是按字符串内容比较,符合业务上“相同字符串视为同一个key”的需求

3.字符串常量池优化,复用性强

  • 大量相同String key会复用常量池对象
  • 减少内存占用,提高比较速度
  • 适合做缓存key、配置key等高频场景

4.使用方便、语义清晰

  • 直观、易读、易序列化
  • 适合作为接口、配置、缓存中的key

5.线程安全

  • 不可变=天然线程安全
  • 多线程环境下作为Map的key不会出问题

String和StringBuffer、StringBuilder的区别是什么?

1.可变性(最核心区别)

String:不可变类

  • 底层是final char[],一旦创建,内容不能修改。
  • 所有看似“修改”的操作(+、replace等)都是新建String对象。

StringBuffer/StringBuilder:可变类

  • 底层是普通数组,会自动扩容,直接在原对象上修改,不产生新对象。

2.线程安全

  • String:不可变→天然线程安全
  • StringBuffer:线程安全,方法加了synchronized同步锁,多线程环境下安全。
  • StringBuilder:线程不安全,无锁,因此速度最快。

3.性能速度

从快到慢:StringBuilder>StringBuffer>String

  • String因为频繁创建对象,最慢
  • StringBuilder无锁,最快
  • StringBuffer有锁,稍慢

4.使用场景

  • 少量字符串操作→用String(简单、直观)
  • 大量字符串拼接/修改
    • 单线程→StringBuilder(推荐)
    • 多线程→StringBuffer

总结

  • String不可变,StringBuffer和StringBuilder可变;
  • StringBuffer线程安全但慢,StringBuilder线程不安全但快;
  • 少量操作用String,大量拼接单线程用StringBuilder,多线程用StringBuffer。

自动装箱与拆箱是什么?

1.定义

  • 自动装箱(Autoboxing)

基本数据类型自动转换成对应的包装类对象

Integer a = 10;  // int → Integer,自动装箱
  • 自动拆箱(Unboxing)

包装类对象自动转换成对应的基本数据类型

int b = a;       // Integer → int,自动拆箱

2.底层原理

编译时,编译器自动帮你调用:

  • 装箱:Integer.valueOf(10)
  • 拆箱:a.intValue()

3.对应关系

基本类型↔包装类

  • byte↔Byte
  • short↔Short
  • int↔Integer
  • long↔Long
  • float↔Float
  • double↔Double
  • char↔Character
  • boolean↔Boolean

4.为什么需要?

  • 集合(List、Map)只能存对象,不能存基本类型

  • 方便代码书写,不用手动new Integer()

5.常见问题

空指针风险

Integer a = null;
int b = a; // 自动拆箱调用 a.intValue(),空指针异常

缓存范围

Integer、Byte、Short、Long在-128~127会被缓存,超出才新建对象。

==比较问题

范围内用==为true,超出范围可能为false;包装类比较值建议用equals()

IO流&反射&序列化

java中IO流分为几种?

Java中的IO流主要有4种分类方式,最常用的是按数据方向和数据类型来划分。

核心分类

1.按数据方向(流的源头/去向)

  • 输入流(Input Stream):数据读入程序(从文件/网络到内存)
  • 输出流(Output Stream):数据写出程序(从内存到文件/网络)

2.按数据类型(流的内容格式)

  • 字节流(Byte Stream):处理任意数据(图片、视频、音频、文本),最底层
  • 字符流(Character Stream):专门处理文本数据,按字符编码读取,更高效

四大核心流类

所有流都继承自以下四个抽象类:

流类型 输入流(Input) 输出流(Output) 特点
字节流 InputStream OutputStream 万能流,处理所有文件,读取单位为字节
字符流 Reader Writer 处理纯文本,按字符读取,自动处理编码

常见子类分类

1.字节流家族

  • 节点流(直接操作源头):FileInputStream、FileOutputStream

  • 处理流(包装节点流,增强功能):
    • BufferedInputStream/BufferedOutputStream(缓冲流,提高效率)
    • DataInputStream/DataOutputStream(读取基本数据类型)
    • ObjectInputStream/ObjectOutputStream(对象序列化

2.字符流家族

  • 节点流:FileReader、FileWriter
  • 处理流
    • BufferedReader/BufferedWriter(缓冲流,按行读取readLine())
    • InputStreamReader/OutputStreamWriter(转换流,字节转字符)

BIO,NIO,AIO有什么区别?

  • BIO:同步阻塞IO,一连接一线程,简单但并发低。
  • NIO:同步非阻塞IO,多路复用,单线程管理多连接,高并发。
  • AIO:异步非阻塞IO,完全异步,操作系统完成后通知,性能更高。

1.BIO(Blocking IO)

特点

  • 同步+阻塞
  • 一个客户端连接,服务端必须开启一个线程处理
  • 连接不发数据,线程就一直阻塞等待

优点

  • 简单易理解,代码好写

缺点

  • 并发能力极低,线程开销大
  • 大量连接会导致OOM、线程切换爆炸

适用场景

  • 连接数少、并发低的场景

2.NIO(Non-blocking IO/New IO)

特点

  • 同步+非阻塞
  • 基于Channel、Buffer、Selector三大组件
  • IO多路复用:一个线程管理多个连接(Selector轮询)
  • 连接没有数据时线程不会阻塞

优点

  • 高并发,线程数远小于连接数
  • 资源消耗低

缺点

  • 编程复杂,需要处理粘包、半包
  • 依然是同步,需要不断轮询

适用场景

  • 高并发、长连接服务(Netty基于NIO)

3.AIO(Asynchronous IO)

特点

  • 异步+非阻塞
  • 操作系统完成IO后主动回调通知
  • 无需轮询,无需用户线程等待

优点

  • 性能最高,资源利用率最高
  • 完全异步,业务代码更简洁

缺点

  • Windows支持好,Linux支持不够成熟
  • 编程模型复杂

适用场景

  • 极高并发、连接极多的场景(目前实际使用不如NIO广泛)

核心对比表

特性 BIO NIO AIO
全称 同步阻塞IO 同步非阻塞IO 异步非阻塞IO
线程模型 一连接一线程 一个线程管理多连接 回调机制,无需轮询
复杂度
并发能力 很高
编程难度 简单 复杂 更复杂
主流框架 传统Socket Netty、Mina 较少使用

序列化和反序列化是什么?为什么要实现Serializable?

1.序列化和反序列化是什么

  • 序列化:把Java对象转换成二进制字节流的过程。
  • 反序列化:把二进制字节流重新恢复成Java对象的过程。

简单说:

  • 序列化=对象→字节数组(方便存盘、网络传输)
  • 反序列化=字节数组→对象(恢复数据)

2.为什么要实现Serializable接口

  • Serializable是一个空标记接口(Marker Interface),没有任何方法。
  • 作用是告诉JVM:这个类的对象允许被序列化。
  • 如果不实现Serializable,直接序列化会抛出:NotSerializableException异常。

3.什么时候需要序列化?

  • 对象需要保存到文件
  • 对象需要在网络上传输(比如RPC、分布式调用)
  • 对象需要存入缓存(Redis等)
  • 会话(Session)持久化

4.扩展

transient关键字

被transient修饰的字段,不会被序列化,反序列化后为默认值(0、null、false)。

serialVersionUID

序列化时的版本号,反序列化时会校验版本是否一致。不写会自动生成,类一修改就变,导致反序列化失败。

static变量不会被序列化

序列化只针对对象实例,静态变量属于类,不参与。

什么是反射机制?

反射是指在程序运行期间,动态获取类的完整信息,并且动态调用对象的方法、操作属性的机制。不需要提前在编译期知道类是什么,运行时再加载、解析、调用。

核心作用

1.运行时动态获取类信息

获取类的全类名、父类、接口、构造器、成员变量、成员方法、注解等。

2.运行时动态创建对象

通过Class.forName()+newInstance()/构造器对象创建实例。

3.运行时动态调用方法、访问属性

包括private私有方法和私有变量,都可以通过反射强制访问。

4.解耦、提高扩展性

框架底层大量使用反射:Spring IOC、MyBatis、JUnit、Tomcat等。

反射常用类(来自java.lang.reflect)

  • Class:代表一个类的字节码对象
  • Constructor:代表构造方法
  • Method:代表成员方法
  • Field:代表成员变量
  • Modifier:获取修饰符(public/private/static等)

简单代码示例

// 1. 获取 Class 对象
Class<?> clazz = Class.forName("java.lang.String");

// 2. 获取所有方法
Method[] methods = clazz.getDeclaredMethods();

// 3. 创建对象
Object str = clazz.newInstance();

// 4. 调用方法
Method method = clazz.getMethod("toUpperCase");
Object result = method.invoke(str);

System.out.println(result);

优点

  • 动态性极强,运行时才确定类与调用,适合框架开发
  • 可以突破访问权限,调用私有成员
  • 高度解耦,提高程序扩展性

缺点

  • 性能比直接调用低
  • 破坏封装性,可以访问私有成员
  • 代码复杂,可读性差

总结

反射允许程序在运行时获取类的完整结构信息,并动态创建对象、调用方法、操作属性,是各种框架实现IOC、AOP、注解驱动的核心基础。

反射机制的应用场景有哪些?

1.框架底层核心(最主要场景)

  • Spring IOC容器:通过XML或注解中的全类名,反射动态创建Bean对象
  • Spring AOP:通过反射获取目标类、目标方法,实现动态代理。
  • MyBatis/Hibernate
    • 根据配置文件反射创建实体类对象
    • 把数据库结果集通过反射封装到实体类中
  • Spring MVC:反射调用Controller中的方法,完成请求映射。

2.注解驱动开发

  • 运行时读取类、方法、字段上的注解信息,根据注解执行不同逻辑。
  • 如:@Autowired、@RequestMapping、@Transactional、@Test等底层都是反射实现。

3.动态代理

  • JDK动态代理在运行时动态生成代理类,并用反射调用目标方法。
  • 是AOP、事务管理、日志增强的基础。

4.通用工具类开发

  • 通用序列化/反序列化:JSON工具(Fastjson、Jackson)通过反射遍历字段,完成对象与 JSON互转。
  • 通用拷贝工具:BeanUtils.copyProperties底层用反射赋值属性。

5.模块化与插件化开发

  • 程序不提前依赖某个类,通过配置文件读取类名,反射加载插件类
  • 实现热插拔、可扩展架构。

6.突破访问限制,操作私有成员

  • 测试框架(JUnit、PowerMock)用反射调用private方法、给private字段赋值。
  • 一些底层工具、兼容代码会用反射获取或修改私有属性。

7.JDBC数据库连接

  • 经典代码:
Class.forName("com.mysql.cj.jdbc.Driver");

就是通过反射加载数据库驱动类。

总结

反射主要用在各种框架底层、注解解析、动态代理、JSON序列化、对象拷贝、插件化开发,是实现“运行时动态性”和“低耦合”的核心技术。

Java获取反射有几种方法?

1.通过对象.getClass()获取

  • 适用于:已经有该类的实例对象
  • 属于Object类中的方法
String str = "hello";
Class<?> clazz = str.getClass();

2.通过类名.class获取

  • 最安全、性能最高
  • 不需要创建对象,编译期就确定
Class<?> clazz = String.class;

3.通过Class.forName("全类名")获取

  • 最常用、最动态,运行时根据字符串加载类
  • 会执行静态代码块
  • 框架底层大量使用
Class<?> clazz = Class.forName("java.lang.String");

扩展:基本类型对应的Class获取

  • 基本类型只能用:类型.class
Class<?> intClazz = int.class;
Class<?> boolClazz = boolean.class;

深拷贝和浅拷贝有什么区别?

  • 浅拷贝(Shadow Copy):只复制引用,不复制引用指向的对象,共用子对象。
  • 深拷贝(Deep Copy):不仅复制对象,还递归复制所有引用的子对象,完全独立。
对比项 浅拷贝 深拷贝
复制内容 基本类型直接复制;引用类型只复制地址 基本类型 + 引用类型指向的对象内容全部复制
内存关系 原对象与拷贝对象共享子对象 原对象与拷贝对象完全独立,互不影响
修改影响 修改子对象会互相影响 修改子对象互不影响
实现方式 clone() 默认、手动赋值、工具类简单拷贝 重写 clone() 递归复制、序列化反序列化
性能 慢(递归 + 创建新对象)

举例

有一个对象里包含一个 List 成员:

  • 浅拷贝:两个对象共用同一个 List,一个改了,另一个也跟着变。
  • 深拷贝:复制出一个新的 List,两个对象各用各的,互不干扰。

总结

  • 浅拷贝:复制对象时,仅复制基本类型值和引用类型的地址,不复制引用指向的实例,原对象与拷贝对象共享引用对象。
  • 深拷贝:复制对象及其所有引用层级的子对象,得到一个完全独立、互不影响的新对象。

如何实现深拷贝?

1. 方式一:重写 clone () 方法(基础,不推荐)

  • 所有对象都重写 clone()
  • 引用类型也调用 clone()
  • 层级多了很麻烦,不推荐
class Person implements Cloneable {
    String name;
    Address addr; // 引用对象

    @Override
    protected Object clone() {
        Person p = (Person) super.clone();
        p.addr = (Address) addr.clone(); // 关键:引用对象也克隆
        return p;
    }
}

2. 方式二:序列化 / 反序列化(最常用、最简单)

把对象写到流里,再读出来,自动深拷贝。

步骤:

  1. 类实现 Serializable
  2. 用对象流读写
public static <T> T deepCopy(T obj) {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(obj);

    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);
    return (T) ois.readObject();
}

优点:一行代码递归深拷贝,不用管层级

缺点:性能一般,必须可序列化

3. 方式三:JSON 序列化(Spring / 项目常用)

用 Jackson / Gson 把对象转 JSON,再转回对象。

ObjectMapper mapper = new ObjectMapper();
User copy = mapper.readValue(mapper.writeValueAsString(user), User.class);

最简单、最实用,项目里最常见。

4. 方式四:构造器 / 手动复制(最安全)

手动 new 对象,逐个复制属性。

public User(User source) {
    this.name = source.name;
    this.age = source.age;
    this.address = new Address(source.address); // 引用类型也new
}

优点:可控、安全、性能好

缺点:代码量大

总结

实现深拷贝最常用的是序列化 / 反序列化,也可以用 JSON 工具类,或手动递归复制所有引用对象。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐