java基础知识(一)
https://zhuanlan.zhihu.com/p/27570687(转)
让人疑惑的Java代码(一)
我们先看一段代码,很简单对不对
![](https://pic2.zhimg.com/80/v2-bdc36dbfa2301fae2df580a7981efcd5_720w.png)
执行一下:
![](https://pic1.zhimg.com/80/v2-222ec8ca49c30041bc1cbcb0084a6538_720w.png)
和大多数人心里想的不一样吧,好多人还在纠结,到底是全是true还是全是false呀。
然而一个是true,一个是false,这是为什么呢?
有的人根本不去思考,直接打开百度搜索去查找答案(一个个都是百度首席软件工程师)
根据Java编译机制,.java文件在编译以后会生成.class文件给JVM加载执行,于是找到.class文件,反编译看了一下,发现编译器在编译我们的代码时,很调皮(聪明的)的在我们声明的变量加上了valueOf方法 ,代码变成了如下:
![](https://pic2.zhimg.com/80/v2-d214c1f88c3339bbcc2ce978e2d1f3e5_720w.png)
看一下valueOf方法的实现
![](https://pic4.zhimg.com/80/v2-979744559eab489c6720ab5659d92a27_720w.png)
我们发现,Integer的作者在写这个类时,为了避免重复创建对象,对Integer值做了缓存,如果这个值在缓存范围内,直接返回缓存好的对象,否则new一个新的对象返回,那究竟这个缓存到底缓存了哪些内容呢?看一下IntegerCache这个类:
![](https://pic4.zhimg.com/80/v2-d2fc88d2223897ecf5b2bee03106f147_720w.png)
这是一个内部静态类,该类只能在Integer这个类的内部访问,这个类在初始化的时候,会去加载JVM的配置,如果有值,就用配置的值初始化缓存数组,否则就缓存-128到127之间的值。
再来看看我们之前的代码:
![](https://pic4.zhimg.com/80/v2-63c64d6212bfcd5fa5da9779049abfab_720w.png)
结论:我们在比较两个Integer对象的值时,无论是怎么声明的,都一定要使用equals去比较,不能用==,在Java中没有重载操作符这一说,特别是从其它语言转到Java的童鞋们要注意。equals我在其它文章里已经做了详细解说,传送门:说说Java里的equals(上) - 知乎专栏
![](https://pic4.zhimg.com/80/v2-75743e8b24978a52b96a0c9788fb8c2f_720w.png)
思考以下代码的执行结果:
![](https://pic3.zhimg.com/80/v2-3f7296c9b34ab1d3c1a41f8eeda9ec7a_720w.png)
Java字符串那些事儿(二)
我们再来看一段代码:
![](https://pic4.zhimg.com/80/v2-09ec8a4e92a26f7101dc4ae067ed43bf_720w.png)
运行一下:
![](https://pic2.zhimg.com/80/v2-c996d02a250a87eaa4530455d6bdd1c9_720w.png)
没错,一个true,一个是false,(答错的小朋友去面壁去),大家可能在想编译器肯定又调皮了,编译的时候是不是又偷偷加了些什么,迫不及待的打开class文件看一下:
![](https://pic4.zhimg.com/80/v2-57ea37ad2fe97705f3c98dc0b15699ab_720w.png)
除了删掉了空行以外和我的java源文件一致呀,这回可冤枉编译器了,那为什么会导致不同的结果呢?我们都知道,Java代码是运行在JVM里的,那是不是JVM在执行这段代码时给我们做了什么?
在JVM中,当代码执行到String s1 = "100" 时,会先看常量池里有没有字符串刚好是“100”这个对象,如果没有,在常量池里创建初始化该对象,并把引用指向它,如下图,绿色部分为常量池,存在于堆内存中。
![](https://pic3.zhimg.com/80/v2-98f2939e7913ee4917c5e1f46bcd4e52_720w.png)
当执行到String s2 = "100" 时,发现常量池已经有了100这个值,于是不再在常量池中创建这个对象,而是把引用直接指向了该对象,如下图:
![](https://pic1.zhimg.com/80/v2-ae152f7f2f7143ac8b1fa178e582cd0c_720w.png)
这时候我们打印System.out.println(s1 == s2)时,由于==是判断两个对象是否指向同一个引用,所以这儿打印出来的就应该是true。
继续执行到Strings3 = new String("100") 这时候我们加了一个new关键字,这个关键字呢就是告诉JVM,你直接在堆内存里给我开辟一块新的内存,如下图所示:
![](https://pic1.zhimg.com/80/v2-4b36f7c2a0944c6a59a68ec8ce84c2d0_720w.png)
继续执行String s4 = new String("100")
![](https://pic2.zhimg.com/80/v2-8da682a5b7395bd992b7d53486423889_720w.png)
这时候再打印System.out.println(s3 == s4) 那一定便是false了,因为s3和s4不是指向对一个引用(对象)。
注:图中只是画出了main方法栈和相关对象在内存中的大致模拟,实际中JVM中内存管理比较复杂,大家有条件的话可以去找《Java虚拟机规范》这本书去深入研究。
结论:我们在比较两个String对象内容时,无论是怎么声明的,都一定要使用equals去比较,不能用==,在Java中没有重载操作符这一说,特别是从其它语言转到Java的童鞋们要注意。equals我在其它文章里已经做了详细解说,传送门:说说Java里的equals(上) - 知乎专栏
![](https://pic4.zhimg.com/80/v2-9922dfe3d4c45470929958184f26334f_720w.png)
说说Java里的equals(上)
在Java字符串那些事儿一文发表后,朋友给我留言说:比较字符串用equals不就完了呗,干嘛要用"==",吃饱了撑的,能不能来点实际的。其实在文章里我是想表明,Java字符串两种声明方式在堆内存中不同的体现,我们在写代码过程中,为了避免重复的创建对象,尽量使用String s1 ="123" 而不是String s1 = new String("123"),因为JVM对前者给做了优化。
那么,我们今天来说说equels,话不多说,上代码:
![](https://pic2.zhimg.com/80/v2-d4e5574096099a4c8e746e7568fbc171_720w.png)
![](https://pic1.zhimg.com/80/v2-19b159f2175e328dac6becfa7894075c_720w.png)
执行一下,结果如下:
![](https://pic4.zhimg.com/80/v2-5747d05f232866686d83609f1e8e3c0b_720w.png)
面试题中老问"=="与和equals有什么区别,甚至连百度搜索equals也会自动关联出equals和"=="的区别这一类的问题来。
![](https://pic4.zhimg.com/80/v2-a7b3f67a95151785079d6f5fc701682f_720w.png)
笔者一直认为,这两者之间没有必然的联系,在引用类型中,"=="是比较两个引用是否指向堆内存里的同一个地址(同一个对象),而equals是一个普通的方法,该方法返回的结果依赖于自身的实现。我们先看一下Person这个类,并没有equals方法呀,那为什么不报错呢?在Java中,如果一个类没有继承其它类,那么它默认继承Object这个类,打开Object这个类看一下,发现如下代码,Person这个类的equals方法就继承自这里
![](https://pic3.zhimg.com/80/v2-c4bf936500c8cd6056ea678357abf41a_720w.png)
很简单,就一句代码,判断两个引用是否指向同一个对象,两个Person对象在堆内存中的表现如下图所示:
![](https://pic1.zhimg.com/80/v2-1acc254722f71915a1018f74a48395a0_720w.png)
所以代码person1.equals(person2)等同于person1 == person2,当然打印出来的结果是false。我们再来看看Integer这个类, equals的实现如下:
![](https://pic2.zhimg.com/80/v2-9f75d738ca75f71317c3d9137fb01db9_720w.png)
当代码执行到System.out.println(itr.equals(lon))时,会判断传入的lon这个对象是否是Integer类型,这里的lon是Long类型,所以打印出来的结果当然是false了。
最后是我们String的实现
![](https://pic4.zhimg.com/80/v2-330cbd581df8d308946da6aabba0667f_720w.png)
当代码执行到:System.out.println(s3.equals(s4)),由于字符串底层char数组里存的都是{'1','0','0'}当然打印出来是true了。
Java中的数组
在说说Java里有equals(上) 这篇文章里,文末我们提到了String底层是char数组来实现的,好多人当年上学时被二维数组,三维数组吓哭了吧。我们今天来讲讲数组,数组非常的重要,很多常用类,比如String等底层都是用数组来实现的,后续我们会一一讲到,多少人很久没用数组了?是否都在用ArrayList呀?这儿先卖个关子,ArrayList底层也是数组实现的。
所谓数组,是相同数据类型的元素按一定顺序排列的集合。现在我们来看一看数组在内存中的样子,话不多说,上代码:
![](https://pic4.zhimg.com/80/v2-35105f000b2343a3cb8053abdb0c6233_720w.png)
这是一段教科书级别的代码,让我想起了中学时候学过的文章,孔乙己问:茴香豆的茴字有几种写法?先编译一下,我们打开编译好的class文件,反编译一下看看:
![](https://pic3.zhimg.com/80/v2-ff2927f3ff98cd43cbbc3e18e6336ab2_720w.png)
三种数组的声明方式编译后,最后创建的方式都是一样的,都给我们加了new关键字,顺手还把charArr3的声明与赋值一体化了,编译器你管得也太多了吧。评论区里有人说反编译后和我反编译后的代码不一样,本专栏所有文章是基于JDK1.8讲解的,反编译工具是idea自带的反编译工具,不一样的原因可能是各位的JDK版本或反编译工具和我不一致。用IDE的代码联想功能看一下:
![](https://pic2.zhimg.com/80/v2-892060ceb35b218c1e7b8c9372a86881_720w.png)
恩,没错,Object类有的方法它都有,它还多了一个length属性(注意不是方法)。个人认为,在Java层面,我们完全可以把数组当成对象来看待,下图我们模拟一下数组在堆内存中的大致的样子,每一个数组都是按顺序排列在堆内存中,正因为如此,我们可以通过数组+[下标]的方式来直接访问数组里的元素。
![](https://pic3.zhimg.com/80/v2-448447e3343294329275aff468b805b2_720w.png)
我们再来看看二维数组:
![](https://pic4.zhimg.com/80/v2-8e1892a0c57b3a3d1ac11707c7356113_720w.png)
这里还是用了三种方式去声明,还是反编译class文件看一下,虽然有点差别,但还是大同小异,都给我们加了new关键字(这次没有把我们的z数组和赋值一体化)。
![](https://pic2.zhimg.com/80/v2-a44b7c61d53417c5b1fb62f9f5384eed_720w.png)
老规矩,我们画一画。
嘿嘿,不就是数组里面套数组嘛,不要被二维这两个字给吓到了,哪有什么二维数组,其实就是二级数组而已。上图中只画出了数组x,有兴趣的朋友可以自行画一下y和z。
![](https://pic2.zhimg.com/80/v2-5ad580bf5b5b89b780fa67fe0dcca09d_720w.png)
思考以下代码的执行结果:
![](https://pic3.zhimg.com/80/v2-2bdb208a144ec486fd024419d6d48b9e_720w.png)
String是一个很普通的类
上一篇我们讲了Java中的数组,其实是为本章的内容做准备的,String这个类是我们在写Java代码中用得最多的一个类,没有之一,今天我们就讲讲它,我们打开String这个类的源码:
![](https://pic2.zhimg.com/80/v2-9f6d15319a32332c0820f7a408ef435d_720w.png)
声明了一个char[]数组,变量名value,声明了一个int类型的变量hash(hash的作用我们后续会讲),话不多说,上代码:
![](https://pic2.zhimg.com/80/v2-4a2ef9fd85a3f73e7764f1a671e960f9_720w.png)
我们点开构造函数看一下:
![](https://pic1.zhimg.com/80/v2-5c9898d60ed3cc041859ff8b8116be24_720w.png)
多年以前,我看到这段代码时我是懵逼的,没错,我现正在准备构造一个String的对象,那original这个对象又是从何而来?是什么时候构造的呢?
在Java中,当值被双引号引起来(如本示例中的"abc"),JVM会去先检查看一看常量池里有没有abc这个对象,如果没有,把abc初始化为对象放入常量池,如果有,直接返回常量池内容。下图是预先处理String str = new String("abc")的参数"abc"
![](https://pic4.zhimg.com/80/v2-cd776d08c1147df346332d42c3ce0427_720w.png)
接下来处理new关键字,在堆内存中开辟空间,由于hash这个字段是int类型的,成员变量初始化默认值为0。
![](https://pic4.zhimg.com/80/v2-c797e99bee769b6755937b98155f51ef_720w.png)
处理构造函数逻辑,hash是值类型,直接赋值,数组为引用类型,直接指向地址。
![](https://pic1.zhimg.com/80/v2-5c9898d60ed3cc041859ff8b8116be24_720w.png)
继续上图
![](https://pic3.zhimg.com/80/v2-4cfe138fb2af4f7d9fc701f72133f26e_720w.png)
最后执行String str2 = new String("abc"),结果如下图:
![](https://pic2.zhimg.com/80/v2-c8a323166922869a17334431d50da4c5_720w.png)
利用IDE的debug功能看一下,char数组里已经有了'a','b','c'这些值。
![](https://pic4.zhimg.com/80/v2-2fbd808468798c4c55877aac63cc4bdf_720w.png)
下面我们来看一下String这个类下面这些常用的API是如何实现的:
![](https://pic2.zhimg.com/80/v2-1497b776b7177ad6e07fd3cc2b92147d_720w.png)
![](https://pic4.zhimg.com/80/v2-e759478158b74811451c4379dbb94877_720w.png)
![](https://pic1.zhimg.com/80/v2-f186fb86275daed9335449cd52cc0540_720w.png)
![](https://pic1.zhimg.com/80/v2-4d5425ac433c509fab149d9c3c29a6d4_720w.png)
![](https://pic2.zhimg.com/80/v2-d0bedc4bb52276ccc07f85d704729b3d_720w.png)
![](https://pic2.zhimg.com/80/v2-e4fd21d2c03d69910ee3b4e69f0b74c5_720w.png)
![](https://pic4.zhimg.com/80/v2-330cbd581df8d308946da6aabba0667f_720w.png)
很简单对吧,可怕的不是源码难读,而是不想,害怕去读源码的心。如果文章得到了你的认可,请为我的文章点赞,你的赞同是我继续下去的动力。
注:文中的图,只是画对象在JVM中大致的样子,以方便大家理解。如果大家想更深层次的研究JVM,推荐大家看深入理解java虚拟机一书,或关注
大神的讲解。
评论区疑问解答:
![](https://pic4.zhimg.com/80/v2-67fe0603c2d13e8815088da627d0a6eb_720w.png)
看如下源码:如果是同一个对象,直接返回true了,就不往下执行了。
![](https://pic2.zhimg.com/80/v2-1575dc4eaaab12ac8b946d6ce5627c79_720w.png)
Java基本数据类型和引用类型
这两天事有点多,不常上知乎,大家有问题可以关注我的公众号:saysayJava,学习中有任何问题或疑惑可以在公众号里直接询问,我都会回复和解答,知乎私信太多,可能不常回复,请谅解,本专栏的所有的示例代码整理完成后也会放到公众号提供下载。
前面的文章有时候会留一些思考题,主要是想让大家多想想文章的内容,所以没留答案,评论区回复也是五花八门。写这篇文章是想再帮大家巩固一下之前的内容,子曾经说过:“温故而知新,可以上王者”。Java中一共有四类八种基本数据类型,看下表:
![](https://pic2.zhimg.com/80/v2-8bf94b624a507463fb542870eb6cae1d_720w.png)
除掉这四类八种基本类型,其它的都是对象,也就是引用类型,包括数组。
来看一段示例代码:
一个Person类,提供了一个构造方法,一些get/set方法:
![](https://pic4.zhimg.com/80/v2-cfd01239a6114d8f72352e7d5b44a41f_720w.png)
下面是测试的main方法:
![](https://pic4.zhimg.com/80/v2-a020f5f1b5b248f200391512cafb7513_720w.png)
先看第一句代码:
![](https://pic2.zhimg.com/80/v2-44bced166052b8938755d5c8d4a9de3d_720w.png)
方法体里声明的基本数据类型在栈内存里,我们画一下
![](https://pic3.zhimg.com/80/v2-c6981f1aa0a68443af10c3de285026ea_720w.png)
继续执行以下代码
![](https://pic4.zhimg.com/80/v2-002755ee9e7ed63dc4b6d42e35bf9b8f_720w.png)
对于基本数据类型来说,赋值(=号)就相当于拷贝了一份值,把int1的值100,拷贝给int2,继续画图
![](https://pic3.zhimg.com/80/v2-fe67275640ecf78439111f7514919a7a_720w.png)
int1=500,直接修改int1的值为500,表现如下图
![](https://pic3.zhimg.com/80/v2-317de060c61f27d6c4093f586fb47c8a_720w.png)
分别打印int1,int2的值,相信没有人会答错,分别是500,100。
再来看数组的初始化
![](https://pic1.zhimg.com/80/v2-4996988f6e9ecd487b5b000a28ceed0c_720w.png)
先初始化arr1,当执行到new这个关键字,会在堆内存分配内存空间,并把该内存空间的地址赋值给arr1。
![](https://pic1.zhimg.com/80/v2-2be66d9d5e691906a89fedd36a7b6c94_720w.png)
继续执行以下代码
![](https://pic4.zhimg.com/80/v2-76163c63cf6ba26ee9ecdf6aaf9e43d3_720w.png)
这儿arr2初始化时并没有new关键字,并不会在堆内存里新开辟一块空间,而是把arr1里存的堆内存地址直接赋值给了arr2,对于引用类型来说,赋值(=号)就相当于拷贝了一份内存地址,也就是说arr1,arr2现在指向了同一块堆内存,表现形势如下图
![](https://pic2.zhimg.com/80/v2-eec4ae0d9e4f0368632f83cdb84390ad_720w.png)
这时候执行如下代码
![](https://pic4.zhimg.com/80/v2-977c39560d6ff1b71196db80b5ff3023_720w.png)
虽然只是修改arr1数组下标位置为3的值
![](https://pic3.zhimg.com/80/v2-0b745eae4cec0d8544a3ed4ac38dbef6_720w.png)
但由于数组arr1和数组arr2指向同一块堆内存,打印arr1[3]和arr2[3]的值,都是8。你答对了吗?
再来看对象的初始化
![](https://pic4.zhimg.com/80/v2-2c5c3e2d89054e2a4996ed800739f357_720w.png)
当看到这个new,这货肯定在堆内存里开辟了一块内存空间,Person里有一个叫name的String对象,String这个对象有点特殊,虽然没有new这个关键字,但还是在堆内存中开辟了一块空间,在String是一个很普通的类一文中已经讲解过了,这里就不再细讲了,String底层是数组实现的,数组也是引用类型,age为基本数据类型,表现如下图
![](https://pic4.zhimg.com/80/v2-84aed37c59f46cd56a46bd6c54623cd3_720w.png)
上图中大框里的内容就是整个Person对象在堆内存中的体现,继续执行以下代码
![](https://pic1.zhimg.com/80/v2-24fb5472676485bc1d96a72f8f02ab9c_720w.png)
没有new关键字,per2不会在堆内存中新开辟空间,和数组一样,也是把per1的内存地址直接赋值给了per2
![](https://pic3.zhimg.com/80/v2-65a1a85bf445923e68edc51fcc950ac2_720w.png)
当我们修改per1的属性的时候
![](https://pic4.zhimg.com/80/v2-dcc8e0743cda50d7e19d05621039902b_720w.png)
如下图两个红框里的内容,给对象(数组也是对象)赋值其实就是相当于引用重新指向一块堆内存,基本数据类型是直接修改值,表现如下图
![](https://pic1.zhimg.com/80/v2-ff22a589b60f01fc3c54f771fb919a20_720w.png)
所以,不管打印per1还是per2的name、age,打印出来的结果都是“李四”、35,这个你也答对了吗?最后,我们来验证一下,结果是不是和文中说的一致。
![](https://pic4.zhimg.com/80/v2-554baf09fbb03374a26c8f9b722ba0fb_720w.png)
结果完全一致,回过头来看看Java里的“==”比较符,结果就不难理解了,代码如下
![](https://pic3.zhimg.com/80/v2-70e234984166abdfbda6307159052da6_720w.png)
结果分别是false,true,true,当==两边是基本数据类型时,==于比较的是两边的两个值是否相等,当==两边是引用类型时比较的是两个内存地址,也可以看成是看这两个引用是否指向堆内存里的同一块地址,如下图
![](https://pic1.zhimg.com/80/v2-01779bec27a57527d24750289eccf4dc_720w.png)
新手在学习Java时,在引用类型上可能经常容易犯错误,如本文中所讲的arr1,arr2,很多人在写代码的时候是想拷贝一份值来用,却不知道在修改arr1的时候,arr2的值也变了。
本篇内容就讲解到这儿,画图不易,希望大家以后多想想变量在内存中的样子,学习起来可以事半功倍。四类八种基本数据类型,本文只列举了int类型,其它的7中基本类型和int的表现形式一致,这里就不一一举例了。
最后,说一下前面的文章留的文末思考,首先是让人疑惑的Java代码一文中的文末思考
![](https://pic1.zhimg.com/80/v2-282f2c80eccec1c721fda91e1822c080_720w.png)
很简单是false
![](https://pic2.zhimg.com/80/v2-96742ec726ac73b9de0dd157eac42bc9_720w.png)
两个对象都是新new出来的,开辟了两块内存空间,i7和i8的引用不是指向堆内存里的同一块地址,因此打印出来是false。
至于Java中的数组一文中的文末思考
![](https://pic3.zhimg.com/80/v2-2bdb208a144ec486fd024419d6d48b9e_720w.png)
相信认真看过本文的朋友都知道打印出来是多少了,还不清楚的,建议重新阅读本文,直到弄明白为止。
注:char数组的打印有点特殊,int数组打印是打印出来一个地址,而char数组是打印数组里的内容。
最后鸣谢评论区的
,本文刚发布时文中的内存地址产生是连续的,经他指出后,我也觉得可能会新手带来歧义,现已经修改完毕,请放心阅读。
Java自动装箱/拆箱
在让人疑惑的Java代码 - 知乎专栏 一文中我们说到编译器自动为我们加上valueOf这个方法吗?忘了没关系,我们来回顾一下,源代码如下:
![](https://pic2.zhimg.com/80/v2-6b9db2a097290e0c9712b349221de989_720w.png)
编译一下,拿到class文件,我们反编译一下,变成了如下代码:
![](https://pic1.zhimg.com/80/v2-0b22adb0055cf642ee3408b9fb5c03c8_720w.png)
这个加上valueOf方法的过程,就是Java中经常说的装箱过程。
Java中一共有四类八种基本数据类型,除掉这几种类型,其它的都是对象,也就是引用类型。在JDK1.5中,给这四类八种基本类型加入了包装类,对应如下:
基本类型 包装类型
第一类:整型
byte Byte
short Short
int Integer
long Long
第二类:浮点型
float Float
double Double
第三类:逻辑型
boolean Boolean
第四类:字符型
char Character
再看如下代码:
![](https://pic2.zhimg.com/80/v2-52f420bf442282dd8a5586800522550d_720w.png)
编译后的class文件:
![](https://pic3.zhimg.com/80/v2-1760060073fcfa843e1af6d5998a72e6_720w.png)
可以看出来,当我们变量声明为对象类型而赋值为基本数据类型时,Java编译器会对我们的基本数据类型进行装箱,而我们的变量声明为基本类型赋值为对象类型时,编译器又会对我们的对象类型进行拆箱处理。似乎大家都商量好了,用valueOf作为装箱方法,拆箱方法就各自表述吧,一般都是基本数据类型加上Value做为拆箱方法,如intValue,longValue,booleanValue,其它包装类型也大同小异,我就不一一测试了,大家自己动手试试吧。
将int的变量转换成Integer对象,这个过程叫做装箱,
反之将Integer对象转换成int类型值,这个过程叫做拆箱。
以上这些装箱拆箱的方法是在编译成class文件时自动加上的,不需要程序员手工介入,因此又叫自动装箱/拆箱。
看到这里,有些朋友会说,呀,好麻烦呀,已经有了基本类型,为什么还要用包装类?
有人说,是为了让Java成为纯面向对象的语言,笑。
我认为,有以下几点:
1、对象是对现实世界的模拟(一切事物皆对象,通过面向对象的方式,将现实世界的事物抽象成对象),在现实中,假设我们去一个系统(数据库)里查询学生李四的年龄,如下图:
![](https://pic4.zhimg.com/80/v2-07da658f1200d0d52efd708fca0c1557_720w.png)
这时候,录入员还没给李四录入年龄这一项,如果我们用int来声明年龄,大家都知道int是要初始化的,默认情况下为0,0是什么意思,没出生吗?(当然也可以用-1来表示未录入,但总感觉有点怪怪的),如果用Integer来表示,就没这个问题了,为null,就是未录入。
2、为泛型提供了支持。
![](https://pic1.zhimg.com/80/v2-98494a6c35a952d8879436d07d63f41c_720w.png)
3、提供了丰富的属性和API
![](https://pic4.zhimg.com/80/v2-1227bdcc70da00b721f91f6a319466f7_720w.png)
![](https://pic3.zhimg.com/80/v2-e46267ccdc2af679279ea6bf7f92fc62_720w.png)
注意,比较两个值是否相等请用equals方法,我在让人疑惑的Java代码 - 知乎专栏 一文中已经说得很清楚了,这里就不深入了。
4、暂时还没想起来,想起来再补充。
下面我们分析一下不同的声明方式在内存中的展现,代码如下:
![](https://pic2.zhimg.com/80/v2-cb54c29618583fa11b59eb41598147e5_720w.png)
表现如下图:
![](https://pic3.zhimg.com/80/v2-a10a9321aee06d9daf3e1d314abee8a6_720w.png)
如果喜欢本系列文章,请为我点赞或顺手分享,您的支持是我继续下去的动力,您也可以在评论区留言想了解的内容,有机会本专栏会做讲解,最后别忘了关注一下我。
面向对象
面试的时候,面试官问,你是怎么理解面向对象的编程?我想很多人会照着面试题来背吧,面向对象呀,不就是继承,封装,多态吗?注意,面试官问到这里,是想听听你对面向对象的理解,而不是让你来背概念。
面向对象,是对现实世界的模拟,下图我们简单模拟了一个动物世界。
![](https://pic4.zhimg.com/80/v2-7d7ace2e1548587a81705eecacb0c1f7_720w.png)
面向对象的三个基本特征之一继承,这里Primat继承了Animal,Person继承了Primat,继承很简单,就不多说了,看以下代码实现,代码中的注释比较重要,请重点看。
![](https://pic1.zhimg.com/80/v2-7f26a9dd54b5fcc0239de5d9c87350ec_720w.png)
![](https://pic2.zhimg.com/80/v2-2dfd40e2fa6361b212d2d05c11402611_720w.png)
![](https://pic1.zhimg.com/80/v2-0fdb756ec25c2df16b200e24cde4b598_720w.png)
![](https://pic4.zhimg.com/80/v2-00692a11f05dd60efd4b89bc96e4ccb3_720w.png)
![](https://pic2.zhimg.com/80/v2-dc4e20d09b7db9b82b80181189dd9d21_720w.png)
![](https://pic3.zhimg.com/80/v2-20345622b8754cf33457d2ac6cec7612_720w.png)
在代码中,不管是动物,鸟类,人类,猴子,我们都可以抽象成类,类是对象的模板,通过new关键字,可以创建一个个对象。仔细看蓝色框里的内容,animal1和animal2,虽然都是同一个形态(Animal),由于指向的是子类对象,当调用同一个eat()方法,运行时会智能匹配到子类的实现,最后得到的结果也不一样,这种形为,我们称之为多态。
多态要满足三个条件。
一、要有继承;(Person继承了Animal,Monkey也继承了Animal)
二、要有重写;(都重写了父类的eat方法)
三、父类引用指向子类对象。(红框内容)
再看下图,利用IDE的代码联想功能看一下,person4这个对象能访问到name、sex属性,eat()、printAge()方法,但无法访问age属性,isLady()方法。是因为我们在该属性和方法前面加了private关键字。隐藏了不想对客户端暴露的age属性和isLady()方法(这里的客户端是main方法),但是我们对客户端提供了一个printAge方法来打印年龄,但在打印年龄前,我们对年龄做了一系列处理(不打印女士年龄)。
![](https://pic1.zhimg.com/80/v2-911b969cd0dc43effa764658cbb0bfcc_720w.png)
对于这种隐藏对象属性和实现细节,仅对外公开指定方法来控制程序中属性的访问和修改,我们称之为封装。(这儿我们没有对age提供set方法,压根没法修改,提供了一个printAge()方法供外部访问)。
在工作中,我们可能会有这么一个需求,当Person对象的名字一致,年龄一致,性别一致,就当成同一人处理。如果之前看过我 说说Java里的equals(上) - 知乎专栏 一文的人,大家应该不会用==或equals直接去做比较,以下是结果。
![](https://pic2.zhimg.com/80/v2-0609840e53e6594694e8a3fc6e467c9d_720w.png)
![](https://pic4.zhimg.com/80/v2-32a1c1383dcace9fa6d58b629d473b97_720w.png)
当用==和equals失败后,开发者可能会写下以下代码去做判断
![](https://pic1.zhimg.com/80/v2-6a0062e1fa4711e881859b0bc4a2b978_720w.png)
写到年龄逻辑的时候发现,由于age属性是私有的,又没有提供getAge( )方法,获取不到age属性,可能会在Person这个类里提供一个getAge( )方法或把age的private关键字去掉,这样虽然也能完成逻辑,但会导致后续使用该类的人再也不调printAge去打印age了,而是直接访问age属性或getAge( )方法去打印,女士的age也就暴露了出去。很显然,这种做法打破了我们之前对age的封装,不建议这么做。
age是私有的,那我们把这段代码挪到Person对象里不就行了?好主意,比较两个对象是否相等,不应该由客户端来决定,而是由对象本身来决定。这也是面向对象的技巧之一。
有了以上思路,我们在Person里扩展了一个方法,
![](https://pic2.zhimg.com/80/v2-66038bca20e9bbb0ab6da76fcb522995_720w.png)
![](https://pic1.zhimg.com/80/v2-60ee9bb061459a04aa391ce6963024b8_720w.png)
![](https://pic3.zhimg.com/80/v2-dbd8b991e659f5233cd9644cec9e1a3e_720w.png)
一切似乎很完美,结果也和我们期望的一样,问题似乎解决了。
我们知道Set里的元素无放入顺序,元素不可重复,请思考以下代码的执行结果:
![](https://pic4.zhimg.com/80/v2-367a435fa2e2956a756ef31a214c678b_720w.png)
希望看完本章后,读者对面向对象有了自己的一些看法和思考,而不是面试题中一味出现的六个字,多态,继承,封装。甚至你可以对面试官莞尔一笑,啊,我还没对象,有大把的时间投入到工作中来。
评论区里,有人说Animal,Primat应该声明为抽象类而不是接口,其实两种方式都是可以的。如果Animal暂时不确定有属性或有具体的形为,建议先声明成接口,因为一个类可以实现多个接口,但只能继承一个类,声明为接口更灵活一些,也使实现类具有更好的扩展性。
如果喜欢本系列文章,请为我点赞或顺手分享,您的支持是我继续下去的动力,您也可以在评论区留言想了解的内容,有机会本专栏会做讲解,最后别忘了关注一下我。
说说Java里的equals(中)
在面向对象 - 知乎专栏 一文中,我们后续留了一个话题,引入了Set,我们知道Set里面的元素是不可以重复的,话不多说,上代码:
![](https://pic4.zhimg.com/80/v2-7055adbee68169654410d8a91149ee4b_720w.png)
精简了上一章中的Person类,保留了isSame()方法;
![](https://pic4.zhimg.com/80/v2-bf0b42cc0b8485c70d3bed9dfcb210b7_720w.png)
想必大家看到类似的代码开始怀疑笔者是不是又挖坑让大伙跳了吧,来看一下结果。
![](https://pic2.zhimg.com/80/v2-f5c5d74add3109de3473bd8919115f8d_720w.png)
没错,有坑,同样是对象,都是放到了set中,一个打印size()是1,另一个打印size()是2。在面向对象 - 知乎专栏 一文中,我们在Person这个类,虽然写了一个isSame()方法来判断业务上是否相等,看上去是解决了当时的问题,然而不知不觉的为自己挖了坑。假设我们现在正在给用户批量发工资,张三出现了两次,虽然我们用Set去了重,但还是会给张三发两次工资。
在Java程序中,有很多的“公约”,我们称之为编程规范,遵守这些规范实现你的代码,会让你避开很多坑。要判断两个对象的内容是否相等,不要自己写方法(isSame())去判断,而是应该重写父类的 equals方法(这里的父类是Object),在说说Java里的equals(上) - 知乎专栏一文中,我们说过String重写了equals()方法,所以这儿打印size结果是1,而Person没有重写,因此Set没法判断这两个"张三"是否是同一个人,打印size结果是2。
我们再看以下代码:
![](https://pic3.zhimg.com/80/v2-6e3c2b0b960e828fbcfcd51ed497d98e_720w.png)
结果当然是全是false(这个应该没人能答错了吧),看结果
![](https://pic2.zhimg.com/80/v2-f1eb992512045255580e40b73893a2ad_720w.png)
下面我们像String一样,重写一下Person的equals方法。
![](https://pic3.zhimg.com/80/v2-4bf4f10945fbad957639120d76d718c2_720w.png)
看起来没问题,别忘 了,如果是重写方法,我们在方法上要加上@Override注解,加上该注解,编译器会帮你检查是否真的覆盖了父类的方法。编译一下,居然报错了。
![](https://pic2.zhimg.com/80/v2-2806acc0ac06fc6046f506988f8a8bad_720w.png)
原来我们跟本就不是重写(覆盖)了父类的equals方法,而是自己又写了一个参数为Person的equals方法,根本不是重写,只是重载了父类的方法而已。
重载:就是在同一个类中,方法的名字相同,但参数个数、参数的类型不同。
重写:它是指子类和父类的关系,子类重写了父类的方法,但方法名、参数类型、参数个数必须相同
下面我们正确的覆盖一下。
![](https://pic4.zhimg.com/80/v2-5879859840e9c25cef8168da65b307f3_720w.png)
我们写一段测代码测试一下,这里我们引入了List。
![](https://pic2.zhimg.com/80/v2-25e96a4506fa4cd67c64bfee831653fd_720w.png)
运行一下,perList里面我们只添加person1,并没有添加person2,但执行perList.contains(person2)打印的结果居然是true(List里面包含了person2),只因为重写了equals()方法,注意:pSet.contains(person2))依旧是false。
![](https://pic1.zhimg.com/80/v2-37421fb8fb2e5cea5d1129290e2d16d4_720w.png)
再执行本文开始那段代码,不出所料,问题依旧
![](https://pic3.zhimg.com/80/v2-83fb74dbb65a9923c81be4e5a4c0dc92_720w.png)
很明显,Person这个类在重写equals()方法后,虽然已经支持List,但还不支持Set。要完美支持HashMap,HashSet,LinkedHashMap,ConcurrentHashMap等这些类,不但要重写equals方法,还需要重写hashCode()方法。
现在我们在Person类里重写一下hashCode()方法
![](https://pic2.zhimg.com/80/v2-2d56ae5f25ee1a84dfc80cab7bd7aba1_720w.png)
再执行一下,终于看到想要的结果了。
![](https://pic2.zhimg.com/80/v2-afa47ecc791ce53e50b47b5ea156f379_720w.png)
再执行一下本开始那段代码,已经是我们想要的结果了。
![](https://pic4.zhimg.com/80/v2-49550b791c562c2af153531f1498d49f_720w.png)
注:本文中提到的HashMap,HashSet,LinkedHashMap,ConcurrentHashMap,List,hashCode等后续专栏会讲解。
总结:当我们在实际业务中需要重写(覆盖)equals方法时,根据规范,我们一定要重写(覆盖)hashCode方法。在实际开发过程中,不建议一上来就重写equals方法,除非你有特殊的需求。
回答评论区的问题
![](https://pic2.zhimg.com/80/v2-b4f5d624a285e22dd4e193ff75598da9_720w.png)
在文中一开始的示例中,person1,person2并不是同一个对象,默认equals方法是继承自Object的,也就相当于==,如果没有额外的需求明确name相同就视为同一个对象处理,就没有必要去重写equals方法了。
如果喜欢本系列文章,请为我点赞或顺手分享,您的支持是我继续下去的动力,您也可以在评论区留言想了解的内容,有机会本专栏会做讲解,最后别忘了关注一下我。