标签:生成 大于 png 类型 链接 app 成员 new 相关
引言:在最近的Java学习中,遇到一些关于字符串的小问题,现在此做一些总结
==
众所周知,在 Java 中如果用 ==
比较两个对象,那就是比较两个对象是否在内存的同一个位置(地址是否相同)。
equals
在 Java 中,所有类的父类 Object 存在一个 equals
方法,String类复写了这个方法,它实现了真正的字符串比较,代码如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
hashCode
通常情况下,当我们重写 equals
方法时,我们需要重写 hashCode
方法 以保持一致性。默认的 hashCode
方法与地址有关。
看到上图,我们简单介绍一下 Java Stack
,Method Area
和 Heap
。这几个我们经常提到的内存区。
Java堆(Heap)
对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。(我们 new 出来的东西都放在这里)
方法区(Method Area)
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
JVM栈(JVM Stacks)
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
讲完了 内存的概念, 我们就可以引入下一个问题了,码来!
public static void main(String[] args) {
String t1 = "abc";
String t2 = new String("abc");
System.out.println(t1==t2);
}
我们看到 t1
和 t2
的值都是 "abc",直觉上来看结果 应该返回 true
,但是运行以后 返回的是 false
,这是为什么呢?(因为他们的地址不同)。那么他们分别在哪呢?这里我们就要引入常量池的概念了。
Java中的常量池,通常指的是运行时常量池,它是方法区的一部分,一个JVM实例只有一个运行常量池,各线程间共享该运行常量池。
Java常量池简介:Java常量池中保存了一份在编译期间就已确定的数据。它里面包括final常量的值(包括成员常量、局部常量和引用常量)、以及对象字面量的值。
在编译期间,每当给常量赋值它就会去检测常量池中是否存在该值,若存在直接返回该值的地址给常量,若不存在则先在常量池中创建该值,再返回该值的地址给常量。因此常量池中不可能出现相等的数据。
一切经final关键字修饰的变量均为常量,final常量必须在定义时就赋初值,否则编译不通过。
对象字面量是指直接以一常量给对象赋值,而不是在堆空间new出一个对象实例。
常见的两种对象字面量:基本类型的包装类对象字面量、String对象字面量。
字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。
当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false,s1和s2的地址不同所以返回了false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4); // true, s3和s4都指向了StringPool中的"aaa"是同一个对象所以返回了 true
PS: 在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError
错误。
接下来让我们再看一个问题:
public static void main(String[] args) {
String a = "hello2";
final String b = "hello";
String d = "hello";
String c = b + 2;
String e = d + 2;
System.out.println((a == c));
System.out.println((a == e));
}
结果返回了:
true
false
为什么呢?这就要引入常量折叠的概念。
编译器优化
技术。编译期常量
加减乘除的运算过程会被折叠对于 String s1 = "1" + "2";
编译器会给你优化成 String s1 = "12";
在生成的字节码中,根本看不到 "1" "2" 这两个东西。
我们通过idea进行验证下
1、源码文件
public static void main(String[] args) {
String s1 = "1"+"2";
}
2、运行后,idea有个out文件夹,找到上面文件的class文件
public static void main(String[] args) {
String s1 = "12";
}
确实如上面所说,编译器会给你进行优化
必须是编译期常量之间进行运算才会进行常量折叠。
编译期常量就是
“编译的时候就可以确定其值的常量”,
编译期常量
。(数字字面量,字符串字面量等)简单运算的结果
也是编译期常量
,如1+2,"a"+"b"。赋值的 final 的基本类型和字符串变量
也是编译期常量。1.第一个栗子
public static void main(String[] args) {
String s1="a"+"bc";
String s2="ab"+"c";
System.out.println(s1 == s2);
}
相信大家都知道了,输出为true
并且只创建了一个 "abc" 字符串对象,且位于字符串常量池中。
2、第二个栗子
public static void main(String[] args) {
String a = "a";
String bc = "bc";
String s1 = "a" + "bc";
String s2 = a + bc;
System.out.println(s1 == s2);
}
这个结果呢?false
s1 是字符串字面量相加,但是 s2 却是两个非 final 的变量相加,所以不会进行常量折叠。
而是根据 String 类特有的 + 运算符重载,变成类似这样的代码 (jdk1.8)
String s2 = new StringBuilder(a).append(b).toString();
这里 toString()
会生成新的String变量,显然用 == 运算符比较是会返回 false。
总结,只要牢记常量折叠主要指的是编译期常量
加减乘除的运算过程会被折叠。
综上所述,在使用 String的比较时,我们最好使用 equals()
方法,而==
是内存上的比较。不过正是它的引入,才致使我们分析学习了,jvm的内存模型,常量池,常量折叠等编译器层面的知识。因祸得福,希望本节课能巩固我们的底层知识,下期再会了!
标签:生成 大于 png 类型 链接 app 成员 new 相关
原文地址:https://www.cnblogs.com/jiaweixie/p/12672267.html