新手在刚开始学习的时候一定会遇到这个问题,那就是== 和 equals 的区别是什么? 这个问题在很多教程和博客会进行讲解并喜欢拿String对象的实例来举例子,而在实际,equ……
新手在刚开始学习的时候一定会遇到这个问题,那就是== 和 equals 的区别是什么?
这个问题在很多教程和博客会进行讲解并喜欢拿String对象的实例来举例子,而在实际,equals 本质上就是==,查看object.equals方法,我们可以知道,equals 默认情况下是引用比较,只是由于而很多类很多类重写了 equals 方法,例如String 和 Integer 把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。而== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用。
而既然提到String类,那么我们便来多说一说String的特殊之处:
关于String
在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,这样带来的好处是提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
String的底层实现
String之所以可以存在常量池中,是因为这是JVM的一种规定。而实际上查看String源码,就可以看到,在 Java 8 中,String 内部使用 char 数组存储数据的;这是本人jdk1.8.0_181版本下String类的部分源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = "".value;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
这里可以看出,实际上String 采用了一个final的char数组,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
而我在oracle官网(https://docs.oracle.com/javase/9/whatsnew/toc.htm#JSNEW-GUID-B6CD8C25-FD93-4CAA-9286-19A39CC0F26A)也看到了这个:
Adopts a more space-efficient internal representation for strings. Previously, the String class stored characters in a char array, using two bytes (16 bits) for each character. The new internal representation of the String class is a byte array plus an encoding-flag field.
This is purely an implementation change, with no changes to existing public interfaces.
See the CompactStrings option of the java command in Java Platform, Standard Edition Tools Reference.
1
2
3
4
5
这是jdk9的更新内容,这里说的是String 类的实现改用更节省空间的 byte 数组存储字符串,同时数组使用 coder 来标识使用了哪种编码。
String 的创建和初始化
当你在创建String对象并进行初始化的时候,若你用的是直接声明赋值的方法,例如:
Sting a = "abc";那么JAVA虚拟机会用String本身的equals方法在字符串池中查找是否已经存在了值为"abc"的这么一个对象,如果有,则不再创建新的对象,直接返回这个在字符串池的"abc"对象的引用(此时没有新建String对象);如果字符串池中不存在"abc"对象,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回(此时新建1个String对象)。
若你使用的是Sting a = new String("abc");那么JVM也会对字符串池进行查找,如果有,则返回在字符串池的"abc"对象的引用,没有则创建再返回引用,而由于使用的是new String方法,那么构造器会在堆内存开辟新空间,再把字符串池中符合要求的对象(也就是"abc")的引用放到所开辟的空间中(new String一定会开辟新空间,也就是如果字符串池有对应的对象,则只创建一个String对象,如果没有则创建两个,一个在字符串池,一个在堆空间)。可以看到,不同的创建和初始化方法可能会产生不同个数的String对象。在这个过程中,可能也会涉及到宏变量(用final定义了并同时指定了初始值,并且这个初始值是在编译时就被确定下来的)和宏替换,这方面的可以单独了解一下,很简单,一看就懂就不说了。
很多博客讲到String a = "ab"+"cd";创建了三个对象,实际上不是的,因为实际上在jvm编译时,如果出现+操作,并且符号左右都是已经确定了的值,那么jvm会认为+是没有作用的,会直接变成String a = "abc"。(我是查看编译文件得出的,不详述)
String拼接
String的值既然是不可变的,那么我们常用到的String的拼接应该并不是我们所看到的那么简单,实际上是因为字符串拼接太常用了,java才支持可以直接用+号进行拼接,在上面说到如果出现+操作,并且符号左右都是已经确定了的值,那么jvm会认为+是没有作用的;但是一般情况下,你一定不会这样进行拼接,一般都会是一些不确定的值;这种情况下,其真正实现的原理是中间通过new创建一个临时的StringBuilder对象,然后调用初始化方法再调用append方法,最后再做一个toString()操作来实现,这里的new StringBuilder和toString(toString方法源码可以看到会建立一个String对象)的内容拷贝都是发生在堆中;因此建议程序有大量字符串拼接(特别是有循环)或者经常改变内容的字符串,最好直接写StringBuilder实现,就不需要底层new很多临时对象了。对系统性能等都会产生影响;同时提醒,当进行字符串拼接的时候,null拼接会变成字符串"null"。
StringBuffer和StringBuilder
我们也知道操作字符串的类有:String、StringBuffer、StringBuilder。其中String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以再次提醒在经常改变字符串内容的情况下最好不要使用 String。而StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer
基本类型包装类也有缓冲池
实际上不止String有缓冲池,基本类型的包装类也有缓冲池,在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。
在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 – 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。
关于boolean(摘自java虚拟机规范)
boolean是基础类型,但它有点特殊。指令集对boolean的支持有限,当编译器把Java代码编译为字节码的时候,会用int或byte来表示boolean。在Java虚拟机中,jvm的规范中是没有boolean类型。这是因为jvm中对boolean值的操作是通过int类型来进行处理的,而boolean数组则是通过byte数组来进行处理,但是在“堆”区,它也可以被表示为位域。(false会用整数零来表示,所有非零整数都表示true)。
使用 += 或者 ++ 运算符可以执行隐式类型转换。
java一般不支持隐式类型转换,但是使用 += 或者 ++ 运算符可以执行隐式类型转换。
public static int test5(){
short a =3;
System.out.println(a);
// a = a + 1; //此处无法编译通过
a += 1;
a ++;
System.out.println(a);
return a;
}
1
2
3
4
5
6
7
8
9
10
11
上面的语句中a 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。所以a = a + 1无法编译通过;但是 a ++;这个操作却能通过编译,这是因为这里a ++相当于将 a + 1 的计算结果进行了向下转型:
a = (short) (a + 1);
关于hashCode
两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?
1
一般情况下, 使用hashcode的目的在于使用一个对象查找另一个对象。在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。两个不同的键值对,哈希值却相等,就是哈希冲突。我们可以举一个例子,比如说来看一下HashSet,因为这是set集合最常用的实现类,在这个过程中我们可以顺便学习一下set集合是如何保证元素不重合的。实际上,HashSet的内部使用HashMap实现,所有放入HashSet中的集合元素都会转为HashMap的Key-value来保存。HashMap使用散列表来存储,也就是数组+链表+红黑树;在这个过程中,每个需要存储的对象都被转化成key-value形式,此时,会对每一个key(null或者对象或者数字或者字符串)调用hashcode()方法计算hashcode值,然后对hashcode值进行运算,确定出在数组上存放的位置,最后再存储。每一个Hash值对应一个数组下标,数组下标是根据hash值和数组长度计算得来,这个下标决定对象的存储位置。如果两个key的hashcode值相同,那么它们的存储位置(是指在散列表中的存储位置)相同。
所以当我们想hashSet存入同一地址元素时,会先进行哈希值比较后(对于同一地址元素,他们哈希值一般情况下是相同的),接着会比较他们的地址是否相同,如果相同就不会调用equals()方法,如果不同,则调用equals()方法。比较过程中,只要地址相同或者equals()返回true,HashSet中add()中map.put(e,PRESENT)==null则返回false,此时会把相同的这个元素舍弃,HashSet添加元素失败。因此,如果向HashSet中添加一个已经存在的元素,新添加的集合元素不会覆盖原来已有的集合元素,也没办法插入已经存在的元素。
而如果这两个key的equals比较返回false,则说明这两个不是同一个对象,这就是哈希冲突;而对于哈希冲突,有开放定址法、链地址法、公共溢出区法等解决方案。具体实现可以自行百度,数据结构都会涉及到的,不难理解。
关于final
final 修饰的类叫最终类,该类不能被继承。
final 修饰的方法不能被重写。如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
关于Math的取整
Math类中提供了三个与取整有关的方法:ceil,floor,round,这些方法的作用于它们的英文名称的含义相对应,例如:ceil的英文意义是天花板,该方法就表示向上取整,Math.ceil(11.3)的结果为12,Math.ceil(-11.6)的结果为-11;floor的英文是地板,该方法就表示向下取整,Math.floor(11.6)的结果是11,Math.floor(-11.4)的结果-12;最难掌握的是round方法,他表示“四舍五入”,算法为Math.floor(x+0.5),即将原来的数字加上0.5后再向下取整,所以,Math.round(11.5)的结果是12,Math.round(-11.5)的结果为-11.Math.round( )符合这样的规律:小数点后大于5全部加,等于5正数加,小于5全不加。
普通类和抽象类的区别:
普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。
接口和抽象类的区别:
实现:
抽象类的子类使用 extends 来继承;
接口必须使用 implements 来实现接口。
构造函数:抽象类可以有构造函数;接口不能有。
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用public修饰;抽象类中的方法可以是任意访问修饰符。
关于clone
clone() 是 Object 的protected方法,它不是public,一个类不显式去重写clone(),其它类就不能直接去调用该类实例的 clone() 方法。应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是Object的一个protected方法。Cloneable接口只是规定,如果一个类没有实现 Cloneable接口又调用了clone()方法,就会抛出CloneNotSupportedException。使用clone()方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java书上讲到,最好不要去使用clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
————————————————
原文链接:https://blog.csdn.net/weixin_43390562/java/article/details/100108520
还没有评论呢,快来抢沙发~