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()时找不到对象(以为不存在)。

相关推荐
聪明的笨猪猪2 小时前
Java SE “语法”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
宇宙的尽头是PYTHON2 小时前
用生活中的实例解释java的类class和方法public static void main
java·开发语言·生活
wei8440678722 小时前
Android实现RecyclerView粘性头部效果,模拟微信账单列表的月份标题平移
android·java·微信·gitee
LB21122 小时前
苍穹外卖-菜品新增、删除
java·服务器·windows
寻星探路2 小时前
Java EE初阶启程记04---线程的状态
java·开发语言·jvm·java-ee
努力也学不会java2 小时前
【Java并发】揭秘Lock体系 -- 深入理解ReentrantLock
java·开发语言·人工智能·python·机器学习·reentrantlock
haokan_Jia3 小时前
【MyBatis-Plus 动态数据源的默认行为】
java·开发语言·mybatis
_院长大人_3 小时前
IDEA 实现SpringBoot热部署(HotSwap和DevTools混用)
java·spring boot·intellij-idea
小信丶4 小时前
Spring 6 的 @HttpExchange 注解:声明式 HTTP 客户端的现代化利器
java·spring·http