[TOC]
0x01 Java 自动拆装箱
自动装箱和拆箱从Java 1.5开始引入,目的是将原始类型值转自动地转换成对应的对象。自动装箱与拆箱的机制可以让我们在Java的变量赋值或者是方法调用等情况下使用原始类型或者对象类型更加简单直接。
详细的解释可以参考: Java中的自动装箱与拆箱
0x02 equals 与 == 的区别
值类型(int,char,long,boolean等)的话
- 都是用 == 判断相等性。
对象引用的话
== 判断引用所指的对象是否是同一个。
equals 方法,是 Object 的成员函数,有些类会覆盖(
override
) 这个方法,用于判断对象的等价性。例如 String 类,两个引用所指向的 String 都是
"abc"
,但可能出现他们实际对应的对象并不是同一个(和 JVM 实现方式有关),因此用 == 判断他们可能不相等,但用 equals 方法判断一定是相等的。
0x03 说一说你对 java.lang.Object 对象中 hashCode 和 equals 方法的理解。在什么场景下需要重新实现这两个方法?
equals方法用于比较对象的内容是否相等
当覆盖了 equals 方法时,比较对象是否相等将通过覆盖后的 equals 方法进行比较(判断对象的内容是否相等)。
hashCode 方法,大多在集合中用到
将对象放入到集合中时,首先判断要放入对象的 hashCode 值与集合中的任意一个元素的 hashCode 值是否相等,如果不相等直接将该对象放入集合中。
如果 hashCode 值相等,然后再通过 equals 方法判断要放入对象与集合中的任意一个对象是否相等,如果 equals 判断不相等,直接将该元素放入到集合中,否则不放入。
父类的 equals ,一般情况下是无法满足子类的 equals 的需求。
- 比如所有的对象都继承 Object ,默认使用的是 Object 的 equals 方法,在比较两个对象的时候,是看他们是否指向同一个地址。但是我们的需求是对象的某个属性相同,就相等了,而默认的 equals 方法满足不了当前的需求,所以我们要重写 equals 方法。
- 如果重写了 equals 方法,就必须重写 hashCode 方法,否则就会降低 Map 等集合的索引速度。
⚠️ 有没有可能 2 个不相等的对象有相同的 hashCode?
可能会发生,这个被称为哈希碰撞。当然,相等的对象,即我们重写了 equals 方法,一定也要重写 hashCode 方法,否则将出现我们在 HashMap 中,相等的对象作为 key ,将找不到对应的 value 。
所以说,equals 和 hashCode 的关系会是:
- equals 不相等,hashCode 可能相等。
- equals 相等,请重写 hashCode 方法,保证 hashCode 相等。
一般来说,hashCode 方法的重写,可以看看 《科普:为什么 String hashCode 方法选择数字31作为乘子》 方法。
0x04 继承和组合的区别
- 继承:指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。在 Java 中,此类关系通过关键字
extends
明确标识,在设计时一般没有争议性。 - 组合:组合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即 has-a 的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。
- 比如,计算机与 CPU 、公司与员工的关系等。
- 表现在代码层面,和关联关系是一致的,只能从语义级别来区分。
因为组合能带来比继承更好的灵活性,所以有句话叫做“组合优于继承”。可以参考 《怎样理解“组合优于继承”以及“OO的反模块化”,在这些方面FP具体来说有什么优势?》
0x05 Java class 的实例化顺序
初始化顺序如下:
- 父类静态变量
- 父类静态代码块
- 子类静态变量、
- 子类静态代码块
- 父类非静态变量(父类实例成员变量)
- 父类构造函数
- 子类非静态变量(子类实例成员变量)
- 子类构造函数
实例参考文章:《Java 类的实例化顺序》
0x06 什么是内部类
简单的说,就是在一个类、接口或者方法的内部创建另一个类。这样理解的话,创建内部类的方法就很明确了。当然,详细的可以看看 《Java 内部类总结(吐血之作)》 文章。
⚠️ 内部类的作用是什么?
内部类提供了更好的封装,除了该外围类,其他类都不能访问。
⚠️ 对于为什么需要内部类?java内部类的好处给了一个解释
首先举一个简单的例子,如果你想实现一个接口,但是这个接口中的一个方法和你构想的这个类中的一个方法的名称,参数相同,你应该怎么办?这时候,你可以建一个内部类实现这个接口。由于内部类对外部类的所有内容都是可访问的,所以这样做可以完成所有你直接实现这个接口的功能。
不过你可能要质疑,更改一下方法的不就行了吗?
的确,以此作为设计内部类的理由,实在没有说服力。
真正的原因是这样的,java中的内部类和接口加在一起,可以的解决常被C++程序员抱怨java中存在的一个问题 没有多继承。实际上,C++的多继承设计起来很复杂,而java通过内部类加上接口,可以很好的实现多继承的效果。
个人认为内部类的存在会影响java源码的可读性,能有更好的设计最好就不要存在内部类
⚠️ Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
可以继承其他类或实现其他接口,在 Java 集合的流式操作中,我们常常这么干。
⚠️ 内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。
0x07 什么是Java序列化
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。
- 可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。
- 序列化是为了解决在对对象流进行读写操作时所引发的问题。
反序列化的过程,则是和序列化相反的过程。
另外,我们不能将序列化局限在 Java 对象转换成二进制数组,例如说,我们将一个 Java 对象,转换成 JSON 字符串,或者 XML 字符串,这也可以理解为是序列化。
⚠️ Java 序列话中,如果有些字段不想进行序列化怎么办?
对于不想进行序列化的变量,使用 transient
关键字修饰。
- 当对象被序列化时,阻止实例中那些用此关键字修饰的的变量序列化。
- 当对象被反序列化时,被
transient
修饰的变量值不会被持久化和恢复。 transient
只能修饰变量,不能修饰类和方法。
0x08 trasient 关键字学习
java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
transient使用小结
1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化
详细案例可以参考:Java transient关键字使用小记
0x09 讲解一下ThreadLocal (非常重要!!!)
源码详解参考:并发容器之ThreadLocal
首先通过实例来理解一下ThreadLocal的用法:
1 | ThreadLocal<String> localName = new ThreadLocal(); |
ThreadLocal可以保证各个线程的数据互不干扰,核心原因见下面的源码:
1 | public void set(T value) { |
由上面的源码我们可以得知ThreadLocal保证各个线程的数据互不干扰的关键在于,每个线程中都有一个ThreadLocalMap
数据结构,当执行set方法时,其值是保存在当前线程的threadLocals
变量中,当执行set方法中,是从当前线程的threadLocals
变量获取。所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。
⚠️ ThreadLocal解决hash冲突
可以通过查看ThreadLocal 的set方法来查看
1 | private void set(ThreadLocal<?> key, Object value) { |
⚠️ ThreadLocal中的内存泄露
先看看Entry的实现:
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免内存泄露
详情参考:一篇文章,从源码深入详解ThreadLocal内存泄漏问题
既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
1 | ThreadLocal<String> localName = new ThreadLocal(); |
0x10 Java 实现对象克隆
在Java中,如果只是单纯的将一个变量赋值给另一个变量,这其实并没有对对象进行克隆,在内存中并没有给新的对象开辟一块新的空间,而只是给已有空间新增了一个引用,这样造成的后果是任何一个引用改变值时,所有的引用的值都会改变(因为它们指向的同一块的内存中的值已经改变)
- demo
1 | class Student { |
直接new一个对象是可以达到类似clone的效果的,但是new对象需要赋值,着很麻烦。更重要的原因在于Object.clone()
方法是一个naive方法,这意味着效率会很高
1 | /* |
浅克隆(ShallowClone)和深克隆(DeepClone)
1、浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。
2、深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。
(如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。)
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
⚠️ BeanUtils()提供的拷贝功能
直接上结论:
1 | 1``、BeanUtils的copyProperties()方法并不是完全的深度克隆,在包含有引用类型的对象拷贝上就可能会出现引用对象指向同一个的情况,且该方法的性能低下,项目中一定要谨慎使用。 |
0x11 Java 反射的用途及实现
Java 反射机制主要提供了以下功能:
- 在运行时构造一个类的对象。
- 判断一个类所具有的成员变量和方法。
- 调用一个对象的方法。
- 生成动态代理。
反射的应用很多,很多框架都有用到:
- Spring 框架的 IoC 基于反射创建对象和设置依赖属性。
- Spring MVC 的请求调用对应方法,也是通过反射。
- JDBC 的
Class#forName(String className)
方法,也是使用反射。
反射的详细用法可以参考:Java 反射由浅入深 | 进阶必备
⚠️ 反射中,Class.forName 和 ClassLoader 区别?
这两者,都可用来对类进行加载。差别在于:
Class#forName(...)
方法,除了将类的.class
文件加载到JVM 中之外,还会对类进行解释,执行类中的static
块。ClassLoader 只干一件事情,就是将
.class
文件加载到 JVM 中,不会执行static
中的内容,只有在 newInstance 才会去执行static
块。Class#forName(name, initialize, loader)
方法,带参函数也可控制是否加载static
块,并且只有调用了newInstance 方法采用调用构造函数,创建类的对象。
详细的测试,可以看看 《Java 反射中,Class.forName 和ClassLoader 的区别(代码说话)》 文章。
0x12 Java 反射
博客中有详细的资料讲解Java元注解、注解属性、Java预置的常用注解、如何编写自定义注解&&使用,如果不熟悉以上内容,可以去博客内详细学习:)
⚠️ Java 元注解
元标签有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。
@Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的取值如下:
RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们
我们可以这样的方式来加深理解,@Retention 去给一张标签解释的时候,它指定了这张标签张贴的时间。@Retention 相当于给一张标签上面盖了一张时间戳,时间戳指明了标签张贴的时间周期。
1 | (RetentionPolicy.RUNTIME) |
@Documented
顾名思义,这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到 Javadoc 中去。
@Target
Target 是目标的意思,@Target 指定了注解运用的地方。
你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。
类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。@Target 有下面的取值
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.CONSTRUCTOR 可以给构造方法进行注解
ElementType.FIELD 可以给属性进行注解
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
ElementType.METHOD 可以给方法进行注解
ElementType.PACKAGE 可以给一个包进行注解
ElementType.PARAMETER 可以给一个方法内的参数进行注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
@Inherited
Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。
说的比较抽象。代码来解释。
1 |
|
@Repeatable
Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
什么样的注解会多次应用呢?通常是注解的值可以同时取多个。
举个例子,一个人他既是程序员又是产品经理,同时他还是个画家。
1 | Persons { |
- 本文作者: Noisy
- 本文链接: http://Metatronxl.github.io/2019/11/29/Java-基础知识储备-二/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!