Java SE “核心类:String/Integer/Object”面试清单(含超通俗生活案例与深度理解)

一、String相关面试题

(一)String是Java基本数据类型吗?可以被继承吗?

• 核心解析:String并非Java的基本数据类型。Java中定义的基本数据类型仅有8种,分别对应不同数据场景:处理小整数的byte、short,处理常规整数的int、long,处理小数的float、double,处理单个字符的char,以及处理逻辑值的boolean。除这8种基本数据类型外,Java中其余所有类(包括String)都属于引用数据类型,使用时需通过"引用"间接操作其对应的对象。此外,String类无法被继承,因为其类定义携带final关键字------在Java规则中,被final修饰的类属于"不可变类",既不能被其他类继承,也不能重写其内部已定义的方法。

• 通俗例子:我们可以用日常生活中的"米"来类比:基本数据类型像"散装米",你去超市买米时,直接用袋子装起散装米就能用(无需额外包装),比如买1斤散装米(对应int 1);而String这类引用数据类型则像"密封袋装米",袋子上印着品牌、重量等信息(对应类的属性和方法),你需要先拿起这袋米(获取引用),才能打开袋子使用里面的米。至于final修饰的作用,就像这袋米是"一次性密封袋"------你无法拆开密封口往里面添加其他东西(修改类的属性),也不能把这个密封袋改成其他样式的包装袋(继承并修改类的结构)。

• 额外思考:Java将String设计为不可变类,背后有两大关键考量。一是"安全复用":String常量池(专门存储重复字符串的内存区域)中的对象可被多个代码片段共享,若String可变,某段代码修改了字符串内容,会导致其他使用该字符串的代码获取错误数据,比如多个地方用"USER"作为用户名前缀,若有代码改了"USER"为"USER_",其他地方的前缀都会出错。二是"线程安全":多线程环境下,不可变对象不会被多个线程同时修改,无需额外加锁就能保证数据安全,比如多线程同时打印同一个日志字符串,不用担心字符串内容被改乱。

(二)String和StringBuilder、StringBuffer的核心区别是什么?

• 核心解析:这三个类均用于Java中的字符串处理,但核心差异集中在"可变性"与"线程安全"两个维度,具体区别如下:

◦ String:属于不可变字符串。一旦创建String对象,其内部存储的字符序列就无法修改,比如你创建了"苹果"这个String对象,若想改成"苹果汁",实际是创建了一个新的"苹果汁"对象,原有的"苹果"对象会成为未被引用的垃圾对象;

◦ StringBuffer:属于可变字符串。内部通过字符数组存储内容,支持直接修改(如用append方法添加字符、insert方法插入字符),且通过synchronized关键字实现加锁,能保证多线程环境下的安全------即多个线程同时修改时,不会出现字符错乱(比如线程1加"汁"、线程2加"酱",不会变成"苹汁果酱"),但加锁和释放锁会消耗系统资源,导致执行速度稍慢;

◦ StringBuilder:属于可变字符串。功能与StringBuffer基本一致,同样支持直接修改字符串内容,但未实现加锁机制,属于"非线程安全"类。正因为省去了锁的开销,其执行速度比StringBuffer快30%~50%(实际测试中,单线程拼接1000次字符串,StringBuilder比StringBuffer快约40%)。

• 通俗例子:我们可以把字符串操作比作"记录家庭购物清单",三个类对应三种不同的记录方式:

◦ String就像"一次性便签纸":你在便签上写了"牛奶、面包",如果家人想加"鸡蛋",你不能在原来的便签上涂改(String不可变),只能换一张新便签重新写"牛奶、面包、鸡蛋",原来的便签就会被扔掉(成为垃圾对象);

◦ StringBuffer像"家庭共用的记事簿":家里有爸妈、孩子多个人(多线程)需要加购物项,记事簿旁边放了一支"专属笔",只有拿到笔的人才能写字(synchronized锁),这样不会出现两个人同时写、导致"牛奶"后面接"蛋面"的错乱情况,但如果多个人都要写,就得排队等笔(锁开销),记录速度会慢一点;

◦ StringBuilder像"你自己的私人笔记本":只有你一个人用(单线程),记录购物清单时不用等笔,想加"鸡蛋"就直接在"牛奶、面包"后面补写,想改"面包"为"全麦面包"就直接划掉重写,不用换本子,记录速度比共用记事簿快很多,但如果让多个人同时写这个笔记本,肯定会写得乱七八糟(线程不安全)。

• 额外思考:实际开发中如何选择这三个类?单线程场景下,若需要频繁修改字符串(比如拼接用户信息:姓名+年龄+地址、循环添加日志内容),优先用StringBuilder,比如后端接口中拼接返回给前端的提示信息"用户[张三](ID:123)登录成功";多线程场景下,比如多线程处理日志文件(多个线程同时往日志字符串里加内容),必须用StringBuffer,避免日志内容错乱;若字符串内容固定不变(比如定义常量"ORDER_STATUS_PAID = "已付款""),直接用String即可,因为String在常量池中的复用性更好,能节省内存。

(三)String str1 = new String("abc")和String str2 = "abc"有什么区别?前者会创建几个对象?

• 核心解析:这两个语句的核心差异在于"是否在堆内存中额外创建对象",具体逻辑可拆解为两步:

  1. 两者都会先检查"字符串常量池"(Java专门用于存储字符串常量的内存区域):若常量池中已存在"abc"这个字符串,就直接使用该常量池对象;若不存在,会先在常量池中创建一个"abc"对象;

  2. 关键区别在后续操作:String str2 = "abc"会直接让str2引用常量池中的"abc"对象,没有其他步骤;而String str1 = new String("abc")会在"堆内存"(Java存储对象实例的主要区域)中额外创建一个新的"abc"对象,然后让str1引用堆内存中的这个新对象。

因此,new String("abc")创建的对象数量为"1个或2个":若常量池中已有"abc",则仅在堆中创建1个对象;若常量池中没有"abc",则先在常量池创建1个、再在堆中创建1个,总共2个对象。

• 通俗例子:我们可以把"字符串常量池"比作"社区楼下的共享零食架","abc"就是一包薯片,str1和str2对应两种拿薯片的方式:

◦ 当你执行String str2 = "abc"时,就像去共享零食架取薯片:先看架子上有没有"abc"薯片,有就直接拿这包(str2引用常量池对象),没有就从仓库拿一包放上去(在常量池创建"abc"),再拿下来;

◦ 当你执行String str1 = new String("abc")时,流程更复杂:你还是先去零食架查"abc"薯片(有就拿,没有就放一包),但拿到后不直接吃,而是找了一个新的密封袋(堆内存),把薯片倒进新袋子里,再在袋子上贴一张写有你名字的标签(堆对象的唯一标识),最后str1引用的是这个"贴了名字的新袋子"(堆中的对象)。

举个具体场景:如果零食架上本来就有"abc"薯片,你用new String的方式,就只多了一个"新密封袋"(1个对象);如果零食架上没有,你就得先放一包薯片到架子上(常量池1个),再装新袋子(堆1个),总共2个对象。

• 额外思考:为什么Java要设计"字符串常量池"?因为字符串是开发中使用最频繁的数据类型之一,很多场景下会重复使用相同的字符串(比如系统提示语"操作成功""参数错误"、配置项"DB_URL"等)。若每次使用都创建新对象,会导致大量重复对象占用内存,比如100个地方用"操作成功",就会创建100个相同的String对象;而常量池能让这些地方共享同一个"操作成功"对象,极大节省内存。但new String的方式会绕过常量池复用,额外创建堆对象,所以实际开发中除非有特殊需求(比如需要两个内容相同但地址不同的对象,用于区分不同来源的字符串),否则尽量用String str = "abc"的方式。

(四)String是不可变类,那字符串拼接(如a + b)是怎么实现的?循环中拼接用什么好?

• 核心解析:String的不可变性指"创建后内容无法修改",但字符串拼接(如a + b)依然可以实现,只是实现方式在JDK8前后有明显优化:

  1. JDK8之前:a + b的拼接会直接创建新的String对象。比如String a = "早上",String b = "好",String c = a + b,会先创建"早上""好"两个对象,拼接时再创建"早上好"这个新对象,原有的"早上""好"对象会成为垃圾;若多次拼接(如a + b + c + d),会创建多个中间对象(比如先创建"早上"+"好"="早上好",再创建"早上好"+"呀"="早上好呀"),既浪费内存又影响性能;

  2. JDK8及以后:Java编译器对+拼接做了优化------会自动将a + b转换为StringBuilder的append方法实现。比如上面的c = a + b,编译后会变成"创建StringBuilder对象→调用append(a)→调用append(b)→调用toString()生成c",这样多次拼接也只会创建一个StringBuilder对象,减少中间对象的产生,性能大幅提升。

但需注意:若在循环中用+拼接(如for(int i=0; i<100; i++){ str += i; }),编译器优化会失效------循环每次都会创建一个新的StringBuilder对象(每次循环都执行"new StringBuilder()"),100次循环就会创建100个StringBuilder,依然浪费资源。因此循环中拼接字符串,建议手动创建一个StringBuilder对象,在循环内反复调用append方法。

• 通俗例子:我们可以把字符串拼接比作"整理旅行攻略",String的不可变性和拼接优化对应不同的整理方式:

◦ JDK8之前的+拼接:就像用"单页纸写攻略"------你在一张纸上写"第一天:去故宫",另一张纸上写"第二天:去长城",想把两天的攻略拼在一起,只能找一张新纸抄一遍"第一天:去故宫,第二天:去长城"(创建新String对象);如果要拼10天的攻略,就得抄10次新纸,原来的单页纸全成了废纸(中间对象),又费纸又费时间;

◦ JDK8之后的+拼接:就像用"活页本贴攻略"------把"第一天""第二天"的攻略页直接贴到活页本上(append方法),最后把活页本装订成一本完整攻略(toString方法),不管拼多少天的攻略,只用一个活页本,不用抄新纸;

◦ 循环中用+拼接:就像你每次贴一天的攻略,都换一个新的活页本------贴"第一天"用本1,贴"第二天"换本2,贴到"第一百天"换本100,最后有100个活页本,比单页纸还浪费;而手动创建一个活页本,循环贴100天的攻略,只用一个本,效率最高。

• 额外思考:那StringBuffer在循环拼接中什么时候用?若循环是多线程场景(比如多个线程同时往一个字符串里加日志内容,线程1加"用户登录",线程2加"订单支付"),手动用StringBuilder会出现"攻略贴乱"的情况(比如"用户订单登录支付"),这时候就需要用StringBuffer------相当于给活页本加了一把锁,每次只有一个人能贴攻略,虽然慢一点,但能保证攻略顺序正确。不过单线程循环中,StringBuilder的性能比StringBuffer高,优先选前者。

(五)String的intern()方法有什么作用?

• 核心解析:intern()方法是String类特有的"常量池关联工具",其核心作用是"让当前String对象与字符串常量池建立绑定关系",具体逻辑如下:

  1. 当调用某个String对象的intern()方法时,会先检查字符串常量池中是否存在"与当前对象内容相同"的字符串(通过equals()方法判断,只要内容一致就算匹配);

  2. 若常量池中存在这样的字符串,会直接返回常量池中该字符串的引用,当前对象则可被垃圾回收;

  3. 若常量池中不存在,会把当前String对象添加到常量池中,然后返回当前对象的引用,后续其他String对象调用intern()时,就能复用这个常量池对象。

简单来说,intern()方法的目的是"让String对象尽可能复用常量池中的资源",减少堆内存中重复字符串对象的创建,节省内存空间。

• 通俗例子:我们可以把intern()方法比作"公司前台的共享文件柜",每个String对象是你手里的一份文件,"文件内容"就是字符串的内容:

◦ 假设你手里有一份"项目进度报告"(当前String对象,内容是"2024Q3项目进度"),想调用intern()方法,就相当于拿着这份报告去前台;

◦ 前台先打开共享文件柜检查:如果柜子里已经有一份"2024Q3项目进度报告"(内容相同),就从柜子里拿一份给你(返回常量池引用),你手里原来的报告就可以扔进碎纸机(被垃圾回收),直接用柜子里的共享报告;

◦ 如果柜子里没有这份报告,前台会把你手里的报告放进柜子里(添加到常量池),然后把你原来的报告还给你(返回当前对象引用)------这样下次再有同事拿"2024Q3项目进度报告",就能直接用柜子里的共享版,不用再重新制作。

举个实际场景:你用new String("test").intern()创建对象时,先在堆中创建"test"对象,调用intern()后,发现常量池没有"test",就把堆中的"test"添加到常量池,返回堆对象引用;下次再写"test".intern(),会直接返回常量池中的"test"引用,不用再创建新对象。

• 额外思考:intern()方法在JDK6和JDK7中有一个关键区别------JDK6中,字符串常量池位于"方法区"(独立于堆内存的区域),调用intern()时,若常量池没有该字符串,会把字符串的"内容"复制到常量池,创建一个新对象;而JDK7及以后,常量池被移到了"堆内存",调用intern()时,若常量池没有,会直接把堆中对象的"引用"添加到常量池,不用复制内容,进一步节省内存。比如JDK7中,String s = new String("a") + new String("b")会在堆中创建"ab"对象,调用s.intern()时,常量池会直接存储堆中"ab"的引用,而不是复制"ab"的内容------这就像前台把你手里报告的"位置标签"贴在文件柜上,不用再复印一份报告放进去,更节省空间。

二、Integer相关面试题

(一)Integer a = 127,Integer b = 127;Integer c = 128,Integer d = 128;a==b和c==d的结果分别是什么?为什么?

• 核心解析:这道题的答案是a==b为true,c==d为false,核心原因是Java中的"Integer缓存池"机制,具体逻辑可拆解为三步:

  1. 明确==的比较规则:对于引用数据类型(如Integer),==比较的是"对象的内存地址"(即是否为同一个对象);对于基本数据类型(如int),==比较的是"值的大小"。

  2. 理解"自动装箱":当你写下Integer a = 127这样的语句时,Java会自动把基本数据类型int的127,转换成Integer对象,这个过程称为"自动装箱"。自动装箱的底层依赖Integer.valueOf(int i)方法,所有Integer对象的创建(除了new Integer())都会经过这个方法。

  3. 缓存池的作用:Integer.valueOf()方法中有一个关键逻辑------Java会预先创建"-128到127"范围内的Integer对象,存储在一个静态数组(即"Integer缓存池")中。当调用valueOf(i)时,若i在-128到127之间,会直接返回缓存池中的已有对象;若i超出这个范围,会新创建一个Integer对象返回。

因此,a = 127和b = 127都返回缓存池中的同一个对象,内存地址相同,所以a==b为true;c = 128和d = 128超出缓存范围,各自创建新对象,内存地址不同,所以c==d为false。

• 通俗例子:我们可以把Integer缓存池比作"公司茶水间的一次性杯子架",杯子架上整齐摆放着编号从"-128"到"127"的杯子(对应缓存池中的Integer对象),每个杯子上的编号就是Integer的值:

◦ 当你要拿一个"127号杯子"(Integer a = 127),茶水间阿姨会直接从架子上取127号杯子递给你;你再要一个"127号杯子"(Integer b = 127),阿姨还是从架子上拿同一个127号杯子------所以a和b拿到的是同一个杯子(内存地址相同),a==b为true;

◦ 当你要拿一个"128号杯子"(Integer c = 128),架子上没有这个编号的杯子,阿姨只能从仓库取一个新的空白杯子,用马克笔写上128号递给你;你再要一个"128号杯子"(Integer d = 128),阿姨又得找一个新杯子写128号------所以c和d拿到的是两个不同的新杯子(内存地址不同),c==d为false。

这里还要注意一个细节:如果用new Integer(127)创建对象,即使值在缓存范围内,也会直接创建新对象。比如Integer b1 = new Integer(127),a==b1会是false------这就像你不找阿姨拿架子上的杯子,而是自己从外面买了一个新杯子,手动写上127号,这个杯子和架子上的127号杯子不是同一个。

• 额外思考:Integer缓存池的范围为什么是-128到127?这是Java的默认设置,主要基于"开发中常用整数集中在小范围"的实践经验------比如用户年龄(0-120)、商品数量(1-100)、数组索引(0-几十)等,缓存这个范围能最大程度减少对象创建,提升程序性能。不过,缓存的最大值(127)是可以通过JVM参数修改的:在启动程序时加上-XX:AutoBoxCacheMax=xxx(比如-XX:AutoBoxCacheMax=200),就能把缓存范围扩大到-128到200。但缓存的最小值(-128)是固定的,无法修改,这是Java源码中硬编码的逻辑,目的是保证基础数据的稳定性。

(二)String怎么转成Integer?原理是什么?

• 核心解析:在Java中,将String转换为Integer主要依赖两个常用方法:Integer.parseInt(String s)和Integer.valueOf(String s),这两个方法的核心原理是"解析字符串中的数字字符,计算出对应的int值",具体逻辑如下:

  1. 两个方法的关联:Integer.valueOf(String s)本质上是"先调用Integer.parseInt(String s)得到int值,再通过自动装箱转换成Integer对象"。比如Integer.valueOf("123"),会先执行parseInt("123")得到int值123,再调用Integer.valueOf(123)(利用缓存池)转换成Integer对象。因此,两者的核心解析逻辑都依赖Integer.parseInt(String s)方法,而parseInt的底层会调用带进制参数的parseInt(String s, int radix)方法(默认radix=10,即十进制)。

  2. parseInt的核心步骤(以十进制为例):

◦ 第一步:检查字符串是否为空,若为空则抛出NumberFormatException(比如parseInt("")会报错);

◦ 第二步:判断字符串是否包含正负号(比如"-123"中的负号、"+456"中的正号),记录正负状态(默认是正数);

◦ 第三步:遍历字符串中的每个字符,将其转换为对应的数字(比如字符'1'转换为1,'2'转换为2),同时检查字符是否为合法的数字字符(比如'a''#'等非数字字符会抛出NumberFormatException);

◦ 第四步:通过"累积计算"得到int值------源码中采用"负累减"的方式(而非直接累加),比如解析"123"时,先从0开始,010 -1 = -1,-110 -2 = -12,-12*10 -3 = -123,最后根据正负状态返回123。这种方式能避免直接累加时超出int的最大值(比如解析"2147483647"时,累加容易出现数值溢出,负累减更安全)。

• 通俗例子:我们可以把String转Integer比作"数存钱罐里的零钱",String就是"写着零钱金额的纸条"(比如"321"),parseInt方法就是"数钱"的过程:

◦ 第一步:先看纸条上有没有字(字符串是否为空),如果是一张空白纸条(空字符串),就说明没有金额,无法数钱(抛异常);

◦ 第二步:看纸条上有没有"欠"字(负号)或"多"字(正号),比如"欠321元"就是负数,"多321元"就是正数,先记下来要算成负数还是正数;

◦ 第三步:逐字看纸条上的数字------"3""2""1",确认每个字都是数字(不是"三""二"这种汉字,也不是"a""b"这种字母),然后把每个字转换成对应的硬币('3'对应3个1元硬币,'2'对应2个1元,'1'对应1个1元);

◦ 第四步:计算硬币总数------源码的"负累减"就像你怕数错,用"反向计数"的方式:"321"是3个100、2个10、1个1,你先算"010 -3 = -3"(相当于先记"欠3个100"),再算"-310 -2 = -32"(加上"欠2个10"),最后算"-32*10 -1 = -321"(加上"欠1个1");如果纸条上没有"欠"字,就把"欠321"改成"有321",得到最终的321元。

而Integer.valueOf("321")就是在数出321元后,把这笔钱放进一个"标注金额的信封"里(转成Integer对象),方便后续用信封传递(引用传递),比如放进"工资信封袋"(集合)里。

• 额外思考:这两个方法有一个关键区别------parseInt返回的是基本数据类型int,valueOf返回的是引用数据类型Integer。实际开发中如何选择?如果需要直接用数值进行计算(比如计算"用户年龄+5""订单金额*2"),用parseInt更高效,因为不用额外进行装箱操作(节省内存和时间);如果需要把值存储到集合中(比如ArrayList<Integer>、HashMap<Integer, String>),用valueOf更方便,因为集合只能存储引用类型,无法存储基本类型。另外,两个方法都只能解析"纯数字字符串",如果字符串中有非数字字符(比如"123a""12.3""-12b"),都会抛出NumberFormatException,所以实际开发中需要用try-catch块捕获异常,比如"用户输入的年龄是字符串,需要先判断是否为纯数字,再转换"。

三、Object相关面试题

(一)Object类有哪些常见方法?分别有什么作用?

• 核心解析:Object类是Java中所有类的"父类"(也称超类),无论是自定义类(如User、Order),还是Java自带类(如String、Integer),都默认继承Object类,因此所有对象都能调用Object的方法。Object类共提供11个方法,按功能可分为6大类,关键方法及作用如下:

  1. 对象比较相关方法:用于判断两个对象的关联关系,核心是hashCode()和equals(Object obj),两者需配合使用(重写equals必须重写hashCode,否则会导致哈希集合异常)。

◦ hashCode():native方法(由C/C++实现),返回一个int类型的"哈希码"------可理解为对象的"简化标识"(类似身份证号的简化版),但可能存在"哈希冲突"(不同对象哈希码相同)。其主要作用是在哈希表(如HashMap、HashSet)中快速定位对象,比如HashMap会根据对象的hashCode确定其存储的"货架位置",不用遍历所有对象找目标,大幅提升查询效率。

◦ equals(Object obj):默认实现是"比较两个对象的内存地址"(即this == obj),返回boolean值。但很多类会重写这个方法,改成"比较对象的内容",比如String类重写后比较字符序列,Integer类重写后比较数值------两个String对象"abc",即使内存地址不同,equals也会返回true。

  1. 对象拷贝相关方法:用于创建对象的"副本",即clone()方法。

◦ clone():native方法,返回当前对象的"浅拷贝"副本------副本是新对象(x.clone() != x为true),且副本的类类型与原对象相同(x.clone().getClass() == x.getClass()为true)。使用clone()有个前提:当前类必须实现Cloneable接口(一个标记接口,无任何方法),否则调用时会抛出CloneNotSupportedException。浅拷贝的特点是:若对象包含引用类型属性(如类中有一个String[]数组),副本会与原对象共享该引用类型属性(修改副本的数组,原对象的数组也会变);若需"深拷贝"(副本与原对象完全独立),需手动重写clone(),对引用类型属性也进行拷贝。

  1. 对象转字符串相关方法:用于将对象转换为可读字符串,即toString()方法。

◦ toString():默认实现返回"类的全限定名@哈希码的十六进制"(如java.lang.Object@1b6d3586),这种格式对开发人员不友好。因此,大部分类会重写toString(),返回对象的关键属性信息,比如User类重写后返回User{id=1, name='张三', age=20},方便调试(打印对象时看到具体属性)和日志输出(记录对象详情)。

  1. 多线程调度相关方法:用于多线程环境下控制线程的等待与唤醒,共3个方法,且均用final修饰,无法重写。

◦ notify():native方法,唤醒"在此对象监视器上等待的一个线程"------对象监视器即对象的"锁"(如synchronized锁定的对象),若多个线程等待该锁,notify()会随机唤醒一个线程,使其进入就绪状态,等待获取锁。

◦ notifyAll():native方法,唤醒"在此对象监视器上等待的所有线程"------与notify()的区别是,它会唤醒所有等待锁的线程,这些线程会竞争获取锁(谁先抢到谁执行)。

◦ wait():有三个重载方法(wait()、wait(long timeout)、wait(long timeout, int nanos)),native方法。作用是让当前线程"释放对象锁,进入等待状态",直到被其他线程调用notify()/notifyAll()唤醒,或等待时间超时(timeout参数指定超时时间,单位为毫秒)。需注意:wait()必须在synchronized代码块或方法中调用(当前线程需持有对象锁),否则会抛IllegalMonitorStateException;且wait()会释放锁,而Thread.sleep()不会释放锁(sleep时线程仍持有锁,其他线程无法获取)。

  1. 反射相关方法:用于获取对象的运行时类信息,即getClass()方法。

◦ getClass():native方法,返回当前对象的"运行时类"(Class对象)------比如new String().getClass()返回java.lang.String.class,new Integer(1).getClass()返回java.lang.Integer.class。该方法用final修饰,无法重写,保证返回的Class对象唯一(每个类在JVM中仅存在一个Class对象)。通过getClass()可获取类的属性(如字段名、类型)、方法(如方法名、参数)、构造器等信息,是Java反射机制的基础(如动态创建对象、调用方法)。

  1. 垃圾回收相关方法:用于对象被回收前释放资源,即finalize()方法。

◦ finalize():protected方法,当JVM的垃圾回收器(GC)判断对象"不可达"(无任何引用指向它)时,会在回收对象内存前调用该方法。其初衷是让对象在被回收前释放资源(如关闭文件流、释放数据库连接),但实际开发中很少使用------因为finalize()的调用时机不确定(GC执行时间不固定),且可能导致对象"复活"(在finalize()中给对象赋值引用,使其重新可达),影响GC效率。JDK9及以后,finalize()已被标记为过时(@Deprecated),推荐用try-with-resources或AutoCloseable接口替代,更可靠地释放资源(如try(InputStream in = new FileInputStream("test.txt")){...},代码结束后自动关闭流)。

• 通俗例子:我们可以把Object类比作"快递箱的通用操作指南",每个对象都是一个快递箱,指南中的方法对应快递箱的通用操作:

◦ hashCode():就像快递箱上的"简易单号"------比如"12345",快递站会按单号分区(1-5000号放A货架,5001-10000号放B货架),快递员不用逐个打开箱子找东西,只需按简易单号找到对应货架,再在货架上找具体快递,大幅节省时间(对应哈希表的快速查询);

◦ equals(Object obj):默认是"比较两个快递箱的简易单号是否相同"(内存地址),重写后是"比较两个快递箱里的东西是否相同"------比如两个快递箱简易单号不同,但里面都是"华为Mate60手机",重写equals后就认为它们相等,适合判断"是否为同一件商品";

◦ clone():就像"复制快递箱"------你有一个装着"衣服"的快递箱,调用clone()会生成一个新快递箱,里面也装着"衣服"(浅拷贝时,衣服是同一件;深拷贝时,衣服是另一件一模一样的),但新箱子的简易单号和原箱子不同;不过复制前要先确认快递箱支持复制(实现Cloneable接口),否则不能复制,比如"易碎品快递箱"可能不支持复制;

◦ toString():就像快递箱的"面单"------默认面单写"快递箱类型@12345"(类名@哈希码),没人知道里面是什么;重写后,面单写"收件人:李四,地址:上海市浦东区,物品:儿童玩具车,重量:2kg"(关键属性),快递员一看就知道该送哪里、里面是什么,方便配送和核对;

◦ notify()/notifyAll()/wait():就像"会议室排队使用规则"------会议室是"对象锁",想开会的人是"线程":wait()是"会议室里有人,我先去外面等,把会议室让给别人用"(释放锁),比如你要开会,发现里面有人,就去走廊等;notify()是"会议室里的人出来了,喊一个等的人进去",比如里面的人开完会,喊走廊里第一个等的人进去;notifyAll()是"会议室里的人出来了,喊所有等的人过来,谁先抢到谁进去";而sleep()是"我在会议室里坐着等,不出去,别人也进不来"(不释放锁),比如你在会议室里玩手机,即使不开会,别人也不能用;

◦ getClass():就像快递箱上的"物品类型标签"------你拿起一个快递箱,看标签知道里面是"电子设备"(String类)还是"生活用品"(Integer类),标签不能改(final修饰),比如电子设备的标签不会变成生活用品,保证你能准确判断物品类型;

◦ finalize():就像"扔快递箱前的检查"------你要把快递箱扔进垃圾桶(GC回收),先打开箱子看有没有遗漏的东西(比如里面还有一个小零件没拿出来,对应未释放的资源),确认没有再扔;但现在小区有专门的"垃圾分类员"(try-with-resources),会帮你检查并处理遗漏物品,不用你自己动手,所以finalize()就用得少了。

• 额外思考:Object类的方法中,多个方法是native方法(如hashCode()、clone()、notify()等),原因是这些方法需要与操作系统或JVM底层交互(如内存分配、线程调度),Java语言无法直接实现,只能通过native关键字调用C/C++编写的底层代码。另外,equals()和hashCode()的"黄金法则"必须牢记:若两个对象的equals()返回true,它们的hashCode()必须相等;若两个对象的hashCode()不相等,它们的equals()一定返回false。若违反这个法则,会导致HashMap、HashSet等哈希集合无法正常工作------比如两个对象equals()为true,但hashCode()不同,HashMap会把它们存到不同货架,导致后续get()时找不到对象(以为不存在)。

相关推荐
陈小桔5 分钟前
idea中重新加载所有maven项目失败,但maven compile成功
java·maven
小学鸡!6 分钟前
Spring Boot实现日志链路追踪
java·spring boot·后端
xiaogg367818 分钟前
阿里云k8s1.33部署yaml和dockerfile配置文件
java·linux·kubernetes
逆光的July34 分钟前
Hikari连接池
java
微风粼粼1 小时前
eclipse 导入javaweb项目,以及配置教程(傻瓜式教学)
java·ide·eclipse
番茄Salad1 小时前
Spring Boot临时解决循环依赖注入问题
java·spring boot·spring cloud
天若有情6731 小时前
Spring MVC文件上传与下载全面详解:从原理到实战
java·spring·mvc·springmvc·javaee·multipart
祈祷苍天赐我java之术1 小时前
Redis 数据类型与使用场景
java·开发语言·前端·redis·分布式·spring·bootstrap
GJGCY1 小时前
技术剖析:智能体工作流与RPA流程自动化的架构差异与融合实现
人工智能·经验分享·ai·自动化·rpa
XiangrongZ1 小时前
江协科技STM32课程笔记(五)— ADC模数转换器
笔记·科技·stm32