Java全栈面试题汇总目录

目录

基础语法

this 关键字用法

在构造方法中使用this关键字时,是否可以省略this关键字?

super 关键字用法

this 与 super 对比

final 有什么用?

final修饰变量时,基本类型和引用类型有什么区别?

final 修饰成员变量 vs 局部变量

final、finally、finalize 区别

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

static关键字作用?

volatile关键字的作用?

transient关键字的作用?

native关键字的作用?

instanceof关键字的作用?

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

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

面向对象

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

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

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

父类构造方法在什么时候会被调用?

子类构造方法中可以多次调用父类构造方法吗?

父类构造方法可以被子类继承吗?

为什么 super () 必须在第一行,且不能和 this () 同时出现?

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 代表当前对象本身,即调用当前方法 / 构造器的那个对象实例。

1. 区分成员变量与局部变量(最常用)

当局部变量(形参)与成员变量同名时,用 this. 明确指成员变量。

class User {
    private String name;

    public void setName(String name) {
        // this.name = 成员变量;右边 name = 局部变量
        this.name = name;
    }
}

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

  • 语法:this(...)
  • 必须放在构造方法第一行
  • 只能在构造器中使用,不能互相循环调用
class User {
    private String name;
    private int age;

    // 无参构造
    public User() {
        this("张三", 18); // 调用本类带参构造
    }

    // 带参构造
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

3. 返回当前对象

在方法中 return this; 支持链式调用。

public User setAge(int age) {
    this.age = age;
    return this;
}

// 使用:user.setAge(20).setName("李四");

4. 作为参数传递当前对象

把当前对象传入其他方法。

public void method() {
    otherMethod(this);
}

5. 调用本类的成员方法

this.method(),一般可省略 this.,但语义更清晰。

关键注意事项

  1. this 只能在实例方法、构造器中使用,static 方法 / 代码块中不能用
  2. this(...) 调用构造器必须是第一句,且不能递归调用
  3. this 本质是指向当前实例的引用,不是对象本身

在构造方法中使用this关键字时,是否可以省略this关键字?

在构造方法里,this 能不能省略,关键看变量名是否重名。

1. 成员变量和局部变量同名 → this 绝对不能省略

class Student {
    private String name;

    public Student(String name) {
        // 这里 this.name 是成员变量,name 是参数
        this.name = name;  
    }
}

如果省略 this.,变成:

name = name;

两个都是局部变量(参数)自己给自己赋值,成员变量根本没被赋值,BUG。

2. 成员变量和局部变量不同名 → this 可以省略

public Student(String stuName) {
    // 等价于 this.name = stuName;
    name = stuName; 
}

编译器能明确区分,省略完全没问题。

3. 调用本类其他构造方法 this(...) → 绝对不能省略

public Student() { // 必须写 this(...),不能只写 (...) this("张三"); }

总结

  • 赋值时:变量同名 → 不能省;不同名 → 可省
  • 调用构造器:this(...) 必须写,不能省
  • 原则:为了可读性和不出错,开发中通常都写上 this

super 关键字用法

super 代表当前对象的父类引用,用于访问父类的成员。

1. 调用父类的构造方法

  • 语法:super(参数);
  • 必须放在子类构造方法第一行
  • 如果不写,编译器会自动隐式添加 super();(调用父类无参构造)
class Father {
    public Father() {
        System.out.println("父类无参构造");
    }
}

class Son extends Father {
    public Son() {
        // super();  这里即使不写,也会默认存在
        System.out.println("子类构造");
    }
}

如果父类没有无参构造,只有有参构造,子类必须显式写 super(...),否则编译报错。

2. 访问父类的成员变量

当子类与父类变量同名时,用 super. 区分父类成员。

class Father {
    String name = "父亲";
}

class Son extends Father {
    String name = "儿子";

    public void test() {
        System.out.println(this.name);   // 儿子(子类)
        System.out.println(super.name);  // 父亲(父类)
    }
}

3. 调用父类的成员方法

用于调用被子类重写的父类方法。

class Father {
    public void say() {
        System.out.println("我是父类");
    }
}

class Son extends Father {
    @Override
    public void say() {
        super.say(); // 调用父类被重写的方法
        System.out.println("我是子类");
    }
}

关键注意事项

  1. super 不能在 static 方法中使用(和 this 一样)
  2. super () 必须在构造方法第一行,且只能出现一次
  3. this(...) 和 super(...) 不能同时出现在同一个构造里(都要求第一行)
  4. this 指向当前对象,super 指向父类成员
  5. 子类构造一定会先执行父类构造(先父后子)

this 与 super 对比

核心区别

对比项 this super
代表含义 当前对象本身 当前对象的父类引用
访问构造方法

this(...) 调用本类其他构造

super(...) 调用父类构造
访问成员 本类成员变量、成员方法 父类成员变量、成员方法
使用位置 实例方法、构造方法 实例方法、构造方法
static 环境 都不能使用 都不能使用
放置位置 构造方法第一行 构造方法第一行
能否共存 同一个构造里,this() 和 super() 只能二选一 同上

详细对比

1. 代表对象

  • this:指向当前类的实例对象
  • super:指向父类的成员,不是父类对象

2. 调用构造方法

  • this(...):调用本类重载的构造方法
  • super(...):调用父类的构造方法
  • 共同点:必须放在构造方法第一行,所以不能同时出现

3. 访问成员

  • this. 变量:访问本类成员变量(同名时优先)
  • this. 方法 ():调用本类方法
  • super. 变量:访问父类成员变量(子类同名时区分)
  • super. 方法 ():调用父类被重写的方法

4. 共同点

  • 都不能在 static 方法 / 静态代码块中使用
  • 都是关键字,不能作为变量名
  • 都只能在实例相关的代码中使用

    final 有什么用?

    final 的 3 个核心作用

    1. 修饰类

    • 类不能被继承
    • 例如:String、Integer 都是 final 类

    2. 修饰方法

    • 方法不能被子类重写(Override)
    • 但可以被继承、可以被调用

    3. 修饰变量

    • 变成常量,只能赋值一次
    • 基本类型:值不可变
    • 引用类型:地址不可变(对象内容仍可变)

    final修饰变量时,基本类型和引用类型有什么区别?

    1. final 修饰基本类型变量

    • 变量保存的值不能改变
    • 一旦赋值,就不能再修改

    2. final 修饰引用类型变量

    • 变量保存的地址值不能变
    • 但对象内部的属性值可以修改
    // 基本类型
    final int a = 10;
    // a = 20; // 报错,值不能改
    
    // 引用类型
    final Student stu = new Student();
    stu.setName("张三"); // 允许,修改的是对象内容
    // stu = new Student(); // 报错,地址不能改

    final 修饰成员变量 vs 局部变量

    一、赋值时机(最核心区别)

    1. final 成员变量

    必须手动赋值,不能使用默认值,只有 3 种赋值位置:

    • 声明时直接赋值
    • 构造代码块中赋值
    • 构造方法中赋值不赋值 → 编译报错

    2. final 局部变量

    • 可以先声明,后赋值
    • 只要在使用前赋值一次即可
    • 只声明不赋值、不使用 → 不报错

    二、默认值

    1. final 成员变量:有默认值,但不允许使用默认值,必须手动赋值
    2. final 局部变量:没有默认值,必须手动赋值才能使用

    三、命名习惯

    1. final 成员变量:全大写 + 下划线:public static final int MAX_VALUE = 100;
    2. final 局部变量:小驼峰,和普通变量一致

    四、权限修饰符

    1. final 成员变量:可以加public / private / protected / default
    2. final 局部变量:不能加任何权限修饰符

    五、能否与 static 联用

    1. final 成员变量:可以static final → 全局常量
    2. final 局部变量:不能用 static

    六、生命周期

    1. final 成员变量:跟随对象创建与销毁
    2. final 局部变量:跟随方法调用存在,方法结束立即销毁

    七、初始化严格性

    1. final 成员变量:强制必须赋值,非常严格
    2. final 局部变量:仅使用前必须赋值,相对宽松

    八、作用域

    1. final 成员变量:作用于整个类
    2. final 局部变量:仅作用于当前方法 / 代码块内

    总结

    final 成员变量必须在声明 / 代码块 / 构造器赋值,能用权限修饰符和 static,生命周期同对象;final 局部变量只需使用前赋值,不能加权限和 static,生命周期同方法。

    final、finally、finalize 区别

    • final:关键字,用于修饰类、方法、变量,表示不可变、不可继承、不可重写
    • finally:关键字,用于异常处理,配合 try-catch,代码一定会执行
    • finalize():Object 类中的方法,用于垃圾回收前做资源释放,已废弃

    1. final(关键字)

    • 修饰类:类不能被继承
    • 修饰方法:方法不能被重写
    • 修饰变量:变量只能赋值一次,变成常量

    2. finally(关键字)

    • 用在 try...catch...finally 结构中
    • 无论是否发生异常、是否 return,finally 块一般都会执行
    • 常用于关闭流、释放连接、解锁等

    3. finalize ()(方法)

    • 是 Object 类中的方法:protected void finalize() throws Throwable
    • 作用:GC 回收对象前,会调用此方法做资源清理
    • 缺点:不确定何时执行、执行次数不确定、性能差、不安全
    • Java 9 已标记为废弃,不推荐使用

    快速对比表

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

    final

    修饰类/方法/变量

    不可变、不可继承

    编译期限制

    finally

    try-catch结构

    保证代码一定执行

    异常处理结束时

    finalize()

    Object成员方法

    对象回收前清理

    GC不确定时机

    总结

    • final 是个修饰符,用来限制类、方法、变量不可变。
    • finally 用在异常里,保证代码一定执行。
    • finalize 是 Object 里的回收方法,用于资源清理,现在已经废弃。

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

    1. switch 支持的类型

    • 基本类型:byte、short、char、int
    • 对应的包装类:Byte、Short、Character、Integer
    • 枚举类型:enum
    • String 类型:JDK 1.7 及以上 才支持

    2. 为什么不支持 long?

    • switch 底层设计上只支持32位及以下整数类型
    • long 是64位,范围太大,底层实现不兼容,编译会直接报错

    3. 为什么支持 byte?

    • byte 可以自动提升为 int,符合 switch 底层要求

    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)

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

    父类构造方法在什么时候会被调用?

    父类构造方法,一定是在子类构造方法执行之前被调用。

    1. 子类构造器第一行,默认隐式调用

    子类构造方法里,即使你没写 super(),编译器也会自动帮你加上:

    子类构造() {
        super();  // 编译器自动插入,调用父类无参构造
        ...
    }

    2. 子类显式写 super(...) 时

    你手动写了 super(参数),就会调用对应的父类有参构造。

    3. 执行顺序永远是:

    先执行父类构造 → 再执行子类构造

    4. 特殊情况:父类没有无参构造

    如果父类只有有参构造、没有无参构造,子类必须手动写 super(实参),否则编译报错。

    总结

    子类对象创建时,父类构造方法会优先于子类构造方法执行,无论子类构造是否写 super(),都会先调用父类构造。

    子类构造方法中可以多次调用父类构造方法吗?

    不可以,子类构造方法中只能调用一次父类构造方法,而且必须是第一行。

    1. 语法强制

    super(...) 必须放在构造方法第一行可执行代码,一个方法不可能有多个 “第一行”,所以只能写一次。

    2. 对象初始化安全

    父类只需要初始化一次,多次调用会导致父类成员被重复初始化,破坏对象状态。

    3. 编译器也会自动检查:

    一个构造方法里出现多次 super(...) 直接编译报错。

    父类构造方法可以被子类继承吗?

    构造方法不能被子类继承。

    1. 构造方法名必须与类名完全一致,父类类名 ≠ 子类类名,语法上就不可能继承。
    2. 构造方法的作用是初始化自身对象,子类有自己的构造逻辑,不能直接继承使用。
    3. Java 语法明确规定:构造方法不能被继承、不能被重写(Override),只能被调用(super)。

    为什么 super () 必须在第一行,且不能和 this () 同时出现?

    1. 为什么 super () 必须是子类构造第一行?

    Java 规定:子类对象必须先完整初始化父类部分,才能初始化子类部分。

    • 子类可能继承、使用父类的成员变量 / 方法
    • 如果父类还没构造完,子类就先执行代码,会出现:
      • 父类变量未初始化
      • 父类逻辑未执行
      • 直接导致状态不安全、空指针、数据错乱

    所以 Java 强制:子类构造的第一条可执行语句必须是 super (),确保父类先完整构造。

    2. 为什么不能和 this () 同时出现?

    • this(...):调用本类其他构造方法
    • super(...):调用父类构造方法
    • 两者都强制要求自己必须是构造方法内第一行代码

    一个方法不可能有两句第一行,语法上就冲突。

    3. 更深层的设计逻辑

    1. 链式保证最终一定会调用父类构造

    • 写 this() → 会跳到本类另一个构造
    • 最终那个构造里,要么写 super(),要么编译器自动加 super()

    2. 避免一个对象被多次初始化父类

    • 如果允许同时写 this () + super (),可能重复初始化父类,破坏对象安全

    3. 语法简洁统一

    • 一个构造只走一条初始化链:要么走本类重载链,要么直接走父类

    总结

    1. 必须放第一行:因为子类依赖父类成员,必须先完成父类初始化,保证对象安全。
    2. 不能和 this () 同时出现:因为 this () 和 super () 都要求自己是第一行,语法冲突;同时也避免父类被重复初始化。

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

    更多推荐