反射(⭐⭐⭐)
说说你对 Java
反射的理解?
答:Java
中的反射首先是能够获取到Java
中要反射类的字节码,获取字节码有三种方法:
Class.forName(className)
类名.class
this.getClass()
然后将字节码中的方法,变量,构造函数等映射成相应的Method
、Filed
、Constructor
等类,这些类提供了丰富的方法可以被我们所使用。
泛型(⭐⭐)
简单介绍一下 java
中的泛型,泛型擦除以及相关的概念,解析与分派?
泛型是 Java SE1.5
的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。Java
语言引入泛型的好处是安全简单。
在Java SE1.5
之前,没有泛型的情况下,通过对类型Object
的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候出现异常,这是一个安全隐患。
泛型的好处是在编译的时候检查类型安全
,并且所有的转换都是自动和隐式的,提高代码的重用率。
泛型的类型参数只能是类类型(包括自定义类),不是简单类型。
同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
泛型的类型参数可以有多个。
泛型的参数类型可以使用
extends
语句。例如,习惯上称为“有界类型”。泛型的参数类型还可以是通配符类型。例如,
Class<?> classType = Class.forName("java.lang.String");
泛型擦除以及相关的概念
泛型信息只存在代码编译阶段,在进入JVM
之前,与泛型相关的信息都会被擦除掉。
在类型擦除的时候,如果泛型类里的类型参数没有指定上限,则会被转换成Object
类型,如果指定了上限,则会被转换成对应的类型上限。
Java
中的泛型基本上都是在编译器这个层次来实现的。生成的Java
字节码中是不包含泛型中的类型信息的。使用泛型的时候加上类型参数,会在编译器在编译的时候擦除掉。这个过程就称为类型擦除。
类型擦除引起的问题及解决方法:
- 先检查,再编译,以及检查编译的对象和引用传递的问题
- 自动类型转换
- 类型擦除与多态的冲突和解决方法
- 泛型类型变量不能是基本数据类型
- 运行时类型查询
- 异常中使用泛型的问题
- 数组(这个不属于类型擦除引起的问题)
- 类型擦除后的冲突
- 泛型在静态方法和静态类中的问题
注解(⭐⭐)
说说你对 Java 注解的理解?
注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记。程序可以利用 Java
的反射机制来了解你的类及各种元素上有无何种标记,针对不同的标记,就去做相应的事件。标记可以加在包,类,字段,方法,方法的参数以及局部变量上。
其他(⭐⭐)
Java 的 char 是两个字节,是怎么存 Utf-8
的字符的?
是否熟悉 Java char 和字符串(初级)
char
是2个字节,utf-8
是1~3个字节- 字符集(字符集不是编码):
ASCII
码与Unicode
码 - 字符 ->
0xd83dde00
(码点)
是否了解字符的映射和存储细节(中级)
人类认知:字符 => 字符集:0x4e2d(char) => 计算机存储(byte):01001110:4e、00101101:2d编码:UTF-16 “中”.getBytes(“utf-6”); -> fe ff 4e 2d:4 个字节,其中前面的 fe ff 只是字节序标志。
是否能触类旁通,横向对比其他语言(高级)
Python2 的字符串:
- byteString = “中”
- unicodeString = u”中”
令人迷惑的字符串长度
1 | emoij = u"表情" |
Java 与 python 3.2 及以下:2 字节 python >= 3.3:1 字节
注意:Java 9 对 latin 字符的存储空间做了优化,但字符串长度还是 != 字符数。
总结
- Java char 不存 UTF-8 的字节,而是 UTF-16。
- Unicode 通用字符集占两个字节,例如“中”。
- Unicode 扩展字符集需要用一对 char 来表示,例如“表情”。
- Unicode 是字符集,不是编码,作用类似于 ASCII 码。
- Java String 的 length 不是字符数。
Java String 可以有多长?
是否对字符串编解码有深入了解(中级)
分配到栈:
1 | String longString = "aaa...aaa"; |
分配到堆:
1 | byte[] bytes = loadFromFile(new File("superLongText.txt"); |
是否对字符串在内存当中的存储形式有深入了解(高级)
是否对 Java 虚拟机字节码有足够的了解(高级)
源文件:*.java
1 | String longString = "aaa...aaa"; |
字节码:*.class
1 | CONSTANT_Utf8_info { |
javac 的编译器有问题,< 65535 应该改为< = 65535。
Java String 栈分配
- 受字节码限制,字符串最终的 MUTF-8 字节数不超过 65535。
- Latin 字符,受 Javac 代码限制,最多 65534 个。
- 非 Latin 字符最终对应字节个数差异较大,最多字节个数是 65535。
- 如果运行时方法区设置较小,也会受到方法区大小的限制。
是否对 java 虚拟机指令有一定的认识(高级)
new String(bytes)内部是采用了一个字符数组,其对应的虚拟机指令是 newarray [int] ,数组理论最大个数为 Integer.MAX_VALUE
,有些虚拟机需要一些头部信息,所以 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8。
Java String 堆分配
- 受虚拟机指令限制,字符数理论上限为 Integer.MAX_VALUE。
- 受虚拟机实现限制,实际上限可能会小于 Integer.MAX_VALUE。
- 如果堆内存较小,也会受到堆内存的限制。
总结
Java String 字面量形式
- 字节码中 CONSTANT_Utf8_info 的限制
- Javac 源码逻辑的限制
- 方法区大小的限制
Java String 运行时创建在堆上的形式
- Java 虚拟机指令 newarray 的限制
- Java 虚拟机堆内存大小的限制
Java 的匿名内部类有哪些限制?
考察匿名内部类的概念和用法(初级)
- 匿名内部类的名字:没有人类认知意义上的名字
- 只能继承一个父类或实现一个接口
包名.OuterClass$1
,表示定位的第一个匿名内部类。外部类加$N
,N
是匿名内部类的顺序。
考察语言规范以及语言的横向对比等(中级)
匿名内部类的继承结构:Java 中的匿名内部类不可以继承,只有内部类才可以有实现继承、实现接口的特性。而 Kotlin 是的匿名内部类是支持继承的,如
1 | val runnableFoo = object: Foo(),Runnable { |
作为考察内存泄漏的切入点(高级)
匿名内部类的构造方法(深入源码字节码探索语言本质的能力):
- 匿名内部类会默认持有外部类的引用,可能会导致内存泄漏。
- 由编译器生成的。
其参数列表包括
- 外部对象(定义在非静态域内)
- 父类的外部对象(父类非静态)
- 父类的构造方法参数(父类有构造方法且参数列表不为空)
- 外部捕获的变量(方法体内有引用外部 final 变量)
Lambda 转换(SAM 类型,仅支持单一接口类型):
如果 CallBack 是一个 interface,不是抽象类,则可以转换为 Lambda 表达式。
1 | CallBack callBack = () -> { |
总结
- 没有人类认知意义上的名字。
- 只能继承一个父类或实现一个接口。
- 父类是非静态的类型,则需父类外部实例来初始化。
- 如果定义在非静态作用域内,会引用外部类实例。
- 只能捕获外部作用域内的 final 变量。
- 创建时只有单一方法的接口可以用 Lambda 转换。
技巧点拨
关注语言版本的变化:
- 体现对技术的热情
- 体现好学的品质
- 显得专业
Java 中对异常是如何进行分类的?
异常整体分类:
Java 异常结构中定义有 Throwable 类。 Exception 和 Error 为其子类。
Error 是程序无法处理的错误,比如 OutOfMemoryError、StackOverflowError。
这些异常发生时, Java 虚拟机(JVM)一般会选择线程终止。
Exception 是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常,程序中应当尽可能去处理这些异常。
运行时异常都是 RuntimeException 类及其子类异常,如 NullPointerException、IndexOutOfBoundsException 等, 这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的, 程序应该从逻辑角度尽可能避免这类异常的发生。
异常处理的两个基本原则**:**
尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。
不要生吞异常。
NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
ClassNotFoundException 的产生原因主要是: Java 支持使用反射方式在运行时动态加载类,例如使用 Class.forName 方法来动态地加载类时,可以将类名作为参数传递给上述方法从而将指定类加载到 JVM 内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException 异常。 解决该问题需要确保所需的类连同它依赖的包存在于类路径中,常见问题在于类名书写错误。 另外还有一个导致 ClassNotFoundException 的原因就是:当一个类已经由某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。通过控制动态类加载过程,可以避免上述情况发生。
NoClassDefFoundError 产生的原因在于: 如果 JVM 或者 ClassLoader 实例尝试加载(可以通过正常的方法调用,也可能是使用 new 来创建新的对象)类的时候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会导致 NoClassDefFoundError. 造成该问题的原因可能是打包过程漏掉了部分类,或者 jar 包出现损坏或者篡改。解决这个问题的办法是查找那些在开发期间存在于类路径下但在运行期间却不在类路径下的类。
String 为什么要设计成不可变的?
String 是不可变的(修改 String 时,不会在原有的内存地址修改,而是重新指向一个新对象),String 用 final 修饰,不可继承,String 本质上是个 final 的 char[] 数组,所以 char[]数组的内存地址不会被修改,而且 String 也没有对外暴露修改char[]数组的方法。不可变性可以保证线程安全以及字符串串常量池的实现。
Java 里的幂等性了解吗?
幂等性原本是数学上的一个概念,即:f(x) = f(f(x)),对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。
幂等性最为常见的应用就是电商的客户付款,试想一下如果你在付款的时候因为网络等各种问题失败了,然后去重复的付了一次,是一种多么糟糕的体验。幂等性就是为了解决这样的问题。
实现幂等性可以使用 Token 机制。
核心思想是为每一次操作生成一个唯一性的凭证,也就是 token。一个 token 在操作的每一个阶段只有一次执行权,一旦执行成功则保存执行结果。对重复的请求,返回同一个结果。
例如:电商平台上的订单 id 就是最适合的 token。当用户下单时,会经历多个环节,比如生成订单,减库存,减优惠券等等。每一个环节执行时都先检测一下该订单 id 是否已经执行过这一步骤,对未执行的请求,执行操作并缓存结果,而对已经执行过的 id,则直接返回之前的执行结果,不做任何操 作。这样可以在最大程度上避免操作的重复执行问题,缓存起来的执行结果也能用于事务的控制等。
为什么 Java 里的匿名内部类只能访问 final 修饰的外部变量?
匿名内部类用法:
1 | public class TryUsingAnonymousClass { |
编译后的结果
1 | class TryUsingAnonymousClass$1 implements MyInterface { |
因为匿名内部类最终会编译成一个单独的类,而被该类使用的变量会以构造函数参数的形式传递给该类,例如:Integer paramInteger,如果变量不定义成 final的,paramInteger 在匿名内部类被可以被修改,进而造成和外部的 paramInteger 不一致的问题,为了避免这种不一致的情况,因次 Java 规定匿名内部类只能访问 final 修饰的外部变量。
讲一下 Java 的编码方式?
为什么需要编码
计算机存储信息的最小单元是一个字节即 8bit,所以能示的范围是 0~255,这个范围无法保存所有的字符,所以要一个新的数据结构 char 来表示这些字符,从char 到 byte 需要编码。
常见的编码方式有以下几种:
ASCII:总共有 128 个,用一个字节的低 7 位表示,031 是控制字符如换行回车删除等;32126 是打印字符,可以通过键盘输入并且能够显示出来。
GBK:码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。
UTF-16:UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。
UTF-8:统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成。
Java 中需要编码的地方一般都在字符到字节的转换上,这个一般包括磁盘 IO 和 网络 IO。
Reader 类是 Java 的 I/O 中读字符的父类,而 InputStream 类是读字节的父类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中处理读取字节到字符的转换,而具体字节到字符解码实现由 StreamDecoder 去实现,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。
String,StringBuffer,StringBuilder 有哪些不同?
三者在执行速度方面的比较:StringBuilder > StringBuffer > String
- String 每次变化一个值就会开辟一个新的内存空间
- StringBuilder:线程非安全的
- StringBuffer:线程安全的
对于三者使用的总结:
如果要操作少量的数据用 String。
单线程操作字符串缓冲区下操作大量数据用 StringBuilder。
多线程操作字符串缓冲区下操作大量数据用 StringBuffer。
String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。
StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。
什么是内部类?内部类的作用。
内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
创建内部类对象并不依赖于外围类对象的创建。
内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
内部类提供了更好的封装,除了该外围类,其他类都不能访问。。
抽象类和接口区别?
共同点
- 是上层的抽象层。
- 都不能被实例化。
- 都能包含抽象的方法,这些抽象的方法用于描述类具备的功能,但是不提供具体的实现。
区别:
- 在抽象类中可以写非抽象的方法,从而避免在子类中重复书写他们,这样可以提高代码的复用性,这是抽象类的优势,接口中只能有抽象的方法。
- 多继承:一个类只能继承一个直接父类,这个父类可以是具体的类也可是抽象类,但是一个类可以实现多个接口。
- 抽象类可以有默认的方法实现,接口根本不存在方法的实现。
- 子类使用 extends 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明方法的实现。子类使用关键字 implements 来实现接口。它需要提供接口中所有声明方法的实现。
- 构造器:抽象类可以有构造器,接口不能有构造器。
- 和普通 Java 类的区别:除了你不能实例化抽象类之外,抽象类和普通 Java 类没有任何区别,接口是完全不同的类型。
- 访问修饰符:抽象方法可以有 public、protected 和 default 修饰符,接口方法默认修饰符是 public。你不可以使用其它修饰符。
- main 方法:抽象方法可以有 main 方法并且我们可以运行它。接口没有 main 方法,因此我们不能运行它。
- 速度:抽象类比接口速度要快,接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。
- 添加新方法:如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。
接口的意义?
规范、扩展、回调。
父类的静态方法能否被子类重写?
不能。子类继承父类后,用相同的静态方法和非静态方法,这时非静态方法覆盖父类中的方法(即方法重写),父类的该静态方法被隐藏(如果对象是父类则调用该隐藏的方法),另外子类可继承父类的静态与非静态方法,至于方法重载我觉得它其中一要素就是在同一类中,不能说父类中的什么方法与子类里的什么方法是方法重载的体现。
抽象类的意义?
为其子类提供一个公共的类型,封装子类中的重复内容,定义抽象方法,子类虽然有不同的实现 但是定义是一致的。
静态内部类、非静态内部类的理解?
静态内部类:只是为了降低包的深度,方便类的使用,静态内部类适用于包含在类当中,但又不依赖与外在的类,不用使用外在类的非静态属性和方法,只是为了方便管理类结构而定义。在创建静态内部类的时候,不需要外部类对象的引用。
非静态内部类:持有外部类的引用,可以自由使用外部类的所有变量和方法。
为什么复写 equals 方法的同时需要复写 hashcode 方法,前者相同后者是否相同,反过来 呢?为什么?
要考虑到类似 HashMap、HashTable、HashSet 的这种散列的数据类型的运用,当我们重写 equals 时,是为了用自身的方式去判断两个自定义对象是否相等,然而如果此时刚好需要我们用自定义的对象去充当 hashmap 的键值使用时,就会出现我们认为的同一对象,却因为 hash 值不同而导致 hashmap 中存了两个对象,从而才需要进行 hashcode 方法的覆盖。
equals 和 hashcode 的关系?
hashcode 和 equals 的约定关系如下:
- 如果两个对象相等,那么他们一定有相同的哈希值(hashcode)。
- 如果两个对象的哈希值相等,那么这两个对象有可能相等也有可能不相等。(需要再通过 equals 来判断)
java 为什么跨平台?
因为 Java 程序编译之后的代码不是能被硬件系统直接运行的代码,而是一种“中间码”——字节码。然后不同的硬件平台上安装有不同的 Java 虚拟机(JVM),由JVM 来把字节码再“翻译”成所对应的硬件平台能够执行的代码。因此对于 Java 编程者来说,不需要考虑硬件平台是什么。所以 Java 可以跨平台。
浮点数的精准计算
BigDecimal 类进行商业计算,Float 和 Double 只能用来做科学计算或者是工程计算。
final,finally,finalize 的区别?
- final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。
- finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。
- finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在JDK 9 开始被标记为 deprecated。Java 平台目前在逐步使用 java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。Cleaner 的实现利用了幻象引用(PhantomReference),这是一种常见的所谓 post-mortem 清理机制。利用幻象引用和引用队列,我们可以保证对象被彻底销毁前做一些类似资源回收的工作,比如关闭文件描述符(操作系统有限的资源),它比 finalize 更加轻量、更加可靠。
静态内部类的设计意图
静态内部类与非静态内部类之间存在一个最大的区别:非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。
没有这个引用就意味着:它的创建是不需要依赖于外围类的。 它不能使用任何外围类的非 static 成员变量和方法。
Java 中对象的生命周期
在 Java 中,对象的生命周期包括以下几个阶段:
创建阶段(Created)
JVM 加载类的 class 文件 此时所有的 static 变量和 static 代码块将被执行加载完成后,对局部变量进行赋值(先父后子的顺序) 再执行 new 方法 调用构造函数 一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段。
应用阶段(In Use)
对象至少被一个强引用持有着。
不可见阶段(Invisible)
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。 简单说就是程序的执行已经超出了该对象的作用域了。
不可达阶段(Unreachable)
对象处于不可达阶段是指该对象不再被任何强引用所持有。 与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被 JVM 等系统下的某些已装载的静态变量或线程或 JNI 等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些 GC root 会导致对象的内存泄露情况,无法被回收。
收集阶段(Collected)
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了 finalize()方法,则会去执行该方法的终端操作。
终结阶段(Finalized)
当对象执行完 finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
对象空间重分配阶段(De-allocated)
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段。
静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?
结论:
java 中静态属性和静态方法可以被继承,但是不可以被重写而是被隐藏。
原因:
静态方法和属性是属于类的,调用的时候直接通过
类名.方法名
完成,不需要继承机制即可以调用。如果子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为”隐藏”。如果你想要调用父类的静态方法和属性,直接通过父类名.方法或变量名完成,至于是否继承一说,子类是有继承静态方法和属性,但是跟实例方法和属性不太一样,存在”隐藏”的这种情况。多态之所以能够实现依赖于继承、接口和重写、重载(继承和重写最为关键)。有了继承和重写就可以实现父类的引用指向不同子类的对象。重写的功能是:”重写”后子类的优先级要高于父类的优先级,但是“隐藏”是没有这个优先级之分的。
静态属性、静态方法和非静态的属性都可以被继承和隐藏而不能被重写,因此不能实现多态,不能实现父类的引用可以指向不同子类的对象。非静态方法可以被继承和重写,因此可以实现多态。
object 类的 equal 和 hashcode 方法重写,为什么?
在 Java API 文档中关于 hashCode 方法有以下几点规定(原文来自 java 深入解析一书):
在 java 应用程序执行期间,如果在 equals 方法比较中所用的信息没有被修改,那么在同一个对象上多次调用 hashCode 方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同。
如果两个对象通过调用 equals 方法是相等的,那么这两个对象调用 hashCode 方法必须返回相同的整数。
如果两个对象通过调用 equals 方法是不相等的,不要求这两个对象调用hashCode 方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生 不同的 hash 值可以提供哈希表的性能。
java 中==和 equals 和 hashCode 的区别?
默认情况下也就是从超类 Object 继承而来的 equals 方法与‘==’是完全等价的,比较的都是对象的内存地址,但我们可以重写 equals 方法,使其按照我们的需求的方式进行比较,如 String 类重写了 equals 方法,使其比较的是字符的序列,而不再是内存地址。在 java 的集合中,判断两个对象是否相等的规则是:
判断两个对象的 hashCode 是否相等。
判断两个对象用 equals 运算是否相等。
Java 的四种引用及使用场景?
- 强引用(FinalReference):在内存不足时不会被回收。平常用的最多的对象,如新创建的对象。
- 软引用(SoftReference):在内存不足时会被回收。用于实现内存敏感的高速缓存。
- 弱引用(WeakReferenc):只要 GC 回收器发现了它,就会将之回收。用于 Map 数据结构中,引用占用内存空间较大的对象。
- 虚引用(PhantomReference):在回收之前,会被放入 ReferenceQueue,JVM 不会自动将该 referent 字段值设置成 null。其它引用被 JVM 回收之后才会被放入 ReferenceQueue 中。用于实现一个对象被回收之前做一些清理工作。
类的加载过程,Person person = new Person();为例进行说明。
- 因为 new 用到了 Person.class,所以会先找到 Person.class 文件,并加载到内存中;
- 执行该类中的 static 代码块,如果有的话,给 Person.class 类进行初始化;
- 在堆内存中开辟空间分配内存地址;
- 在堆内存中建立对象的特有属性,并进行默认初始化;
- 对属性进行显示初始化;
- 对对象进行构造代码块初始化;
- 对对象进行与之对应的构造函数进行初始化;
- 将内存地址付给栈内存中的 p 变量。
JAVA 常量池
Interger 中的 128(-128~127)
当数值范围为-128~127 时:如果两个 new 出来的 Integer 对象,即使值相同,通过“==”比较结果为 false,但两个对直接赋值,则通过“==”比较结果为“true,这一点与 String 非常相似。
当数值不在-128~127 时,无论通过哪种方式,即使两对象的值相等,通过“==” 比较,其结果为 false;
当一个 Integer 对象直接与一个 int 基本数据类型通过“==”比较,其结果与第一点相同;
Integer 对象的 hash 值为数值本身;
为什么是-128-127?
在 Integer 类中有一个静态内部类 IntegerCache,在 IntegrCache 类中有一个Integer 数组,用以缓存当前数值范围为-128~127 时的 Integer 对象。
在重写 equals 方法时,需要遵循哪些约定,具体介绍一下?
重写 equals 方法时需要遵循通用约定:自反性、对称性、传递性、一致性、非空性
自反性
对于任何非 null 的引用值 x,x.equals(x)必须返回 true。—这一点基本上不会有啥问题
对称性
对于任何非 null 的引用值 x 和 y,当且仅当 x.equals(y)为 true 时,y.equals(x)也为 true。
传递性
对于任何非 null 的引用值 x、y、z。如果 x.equals(y)==true,y.equals(z)==true, 那么 x.equals(z)==true。
一致性
对于任何非 null 的引用值 x 和 y,只要 equals 的比较操作在对象所用的信息没有被修改,那么多次调用 x.equals(y)就会一致性地返回 true,或者一致性的返回false。
非空性
所有比较的对象都不能为空。