Java基础2——深入理解基本数据类型与常量池

基本数据类型与常量池

基本数据类型

  Java中的基本数据类型只有8个:byte(1字节=8位)、short(2字节)、int(4字 节)、long(8字节)、float(4字节)、double(8字节)、char(1字 节)、boolean(1位)。
  除了以上8种基本数据类型,其余的都是引用数据类型。
  对应的包装类分别是:Byte、Short、Integer、Long、Float、Double、 Character、Boolean。

JVM的内存区域组成

java把内存分两种:一种是栈内存,另一种是堆内存

  1. 在函数中定义的基本类型变量(即基本类型的局部变量)和对象的引用变量(即对象的变量名)都在函数的栈内存中分配;
  2. 堆内存用来存放由new创建的对象和数组以及对象的实例变量(即全局变量)

  在函数(代码块)中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间;在堆中分配的内存由java虚拟机的自动垃圾回收器来管理。

堆和栈的优缺点

堆的优势:是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。
缺点:就是要在运行时动态分配内存,存取速度较慢;
栈的优势:是存取速度比堆要快,仅次于直接位于CPU中的寄存器。另外,栈数据可以共享。
缺点:是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
方法区
  方法区中主要存储所有对象数据共享区域,存储静态变量和普通方法、静态方法、常量、字符串常量(严格说存放在常量池,堆和栈都有)等类信息,、说白了就是保存类的模版。(方法区包含所有的class和static变量

自动拆箱和装箱

  自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。
  因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。
原始类
byte,short,char,int,long,float,double和boolean
对应的封装类Byte,Short,Character,Integer,Long,Float,Double,Boolean。

赋值

在Java 1.5以前我们需要手动地进行转换才行,而现在所有的转换都是由编译器来完成。

//before autoboxing
Integer iObject = Integer.valueOf(3);
Int iPrimitive = iObject.intValue()

//after java5
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion

方法调用

public static Integer show(Integer iParam){
   System.out.println("autoboxing example - method invocation i: " + iParam);
   return iParam;
}

//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer

对象相等比较

public class AutoboxingTest {

    public static void main(String args[]) {

        // Example 1: == comparison pure primitive – no autoboxing
        int i1 = 1;
        int i2 = 1;
        System.out.println("i1==i2 : " + (i1 == i2)); // true

        // Example 2: equality operator mixing object and primitive
        Integer num1 = 1; // autoboxing
        int num2 = 1;
        System.out.println("num1 == num2 : " + (num1 == num2)); // true

        // Example 3: special case - arises due to autoboxing in Java
        Integer obj1 = 1; // autoboxing will call Integer.valueOf()
        Integer obj2 = 1; // same call to Integer.valueOf() will return same
                            // cached Object

        System.out.println("obj1 == obj2 : " + (obj1 == obj2)); // true

        // Example 4: equality operator - pure object comparison
        Integer one = new Integer(1); // no autoboxing
        Integer anotherOne = new Integer(1);
        System.out.println("one == anotherOne : " + (one == anotherOne)); // false

    }

}

Output:
i1==i2 : true
num1 == num2 : true
obj1 == obj2 : true
one == anotherOne : false

  值得注意的是第三个小例子,这是一种极端情况。obj1和 obj2的初始化都发生了自动装箱操作。但是处于节省内存的考虑,JVM会缓存-128到 127的 Integer对象。因为 obj1和 obj2实际上是同一个对象。所以使用”==“比较返回 true。
  另一个需要避免的问题就是混乱使用对象和原始数据值,一个具体的例子就是当我们在一个原始数据值与一个对象进行比较时,如果这个对象没有进行初始化或者为Null,在自动拆箱过程中obj.xxxValue,会抛出NullPointerException。


public class Test {

	//基本数据类型的常量池是-128到127之间。
	// 在这个范围中的基本数据类的包装类可以自动拆箱,比较时直接比较数值大小。
	public static void main(String[] args) {
	    //int的自动拆箱和装箱只在-128到127范围中进行,超过该范围的两个integer的 == 判断是会返回false的。
	    Integer a1 = 128;
	    Integer a2 = -128;
	    Integer a3 = -128;
	    Integer a4 = 128;
	    System.out.println(a1 == a4);
	    System.out.println(a2 == a3);

	    Byte b1 = 127;
	    Byte b2 = 127;
	    Byte b3 = -128;
	    Byte b4 = -128;
	    //byte都是相等的,因为范围就在-128到127之间
	    System.out.println(b1 == b2);
	    System.out.println(b3 == b4);

	    //
	    Long c1 = 128L;
	    Long c2 = 128L;
	    Long c3 = -128L;
	    Long c4 = -128L;
	    System.out.println(c1 == c2);
	    System.out.println(c3 == c4);

	    //char没有负值
	    //发现char也是在0到127之间自动拆箱
	    Character d1 = 128;
	    Character d2 = 128;
	    Character d3 = 127;
	    Character d4 = 127;
	    System.out.println(d1 == d2);
	    System.out.println(d3 == d4);


	    Integer i = 10;
	    Byte b = 10;
	    //比较Byte和Integer.两个对象无法直接比较,报错
	    //System.out.println(i == b);
	    System.out.println("i == b " + i.equals(b));
	    //答案是false,因为包装类的比较时先比较是否是同一个类,不是的话直接返回false.
	    int ii = 128;
	    short ss = 128;
	    long ll = 128;
	    char cc = 128;
	    System.out.println("ii == bb " + (ii == ss));
	    System.out.println("ii == ll " + (ii == ll));
	    System.out.println("ii == cc " + (ii == cc));
	    //这时候都是true,因为基本数据类型直接比较值,值一样就可以。

	}
}

基本数据类型的存储方式

**上面自动拆箱和装箱的原理其实与常量池有关**。
存在栈中:
public void(int a)
{
int i = 1;
int j = 1;
}
方法中的i 存在虚拟机栈的局部变量表里,i是一个引用,j也是一个引用,它们都指向局部变量表里的整型值 1.
int a是传值引用,所以a也会存在局部变量表。

存在堆里:
class A{
int i = 1;
A a = new A();
}
i是类的成员变量。类实例化的对象存在堆中,所以成员变量也存在堆中,引用a存的是对象的地址,引用i存的是值,这个值1也会存在堆中。可以理解为引用i指向了这个值1。也可以理解为i就是1.

包装类对象怎么存
其实我们说的常量池也可以叫对象池。
比如String a= new String("a").intern()时会先在常量池找是否有“a"对象如果有的话直接返回“a"对象在常量池的地址,即让引用a指向常量”a"对象的内存地址。
public native String intern();
Integer也是同理
--------------------- 
作者:How 2 Play Life 
来源:CSDN 
原文:https://blog.csdn.net/a724888/article/details/80048774 
版权声明:本文为博主原创文章,转载请附上博文链接!

基本数据类型之间转换

自动类型转换: 容量小的类型自动转换为容量大的数据类型,如下图:在这里插入图片描述

  1. 多种类型的数据混合运算时,系统先自动将所有数据转换成容量最大的那种 数据类型,再进行计算。
  2. 当把任何基本类型的值和字符串值进行连接运算时(+),基本类型的值将自 动转化为字符串类型。当把任何基本类型的值和字符串值进行连接运算时(+),基本类型的值将自动转化为字符串类型。

强制类型转换: 自动类型转换的逆过程,将容量大的转换为容量小的数据类 型。

  1. 使用时要加上强制转换符“()”,但可能造成精度降低或溢出。
  2. 字符串不能直接转换为基本类型,需要借助基本类型对应的包装类来实现。
public class Test {
	public static void main(String[] args){
		// java中的类型的自动转化
		byte b1 = 4;
		int x1 = 3;
		x1 = x1 + b1;
		System.out.println(x1);//x=7
		//java中的强制转化
		byte b2 =3;
		b2 = b2+1;// 编译错误,需要强转
		b2 = (byte)(b2+1);
		System.out.println(b2);//cannot convert from int to byte
	
		char ch = 1;
		char ch2 = 'a';
		ch = ch + ch2;//编译失败
		ch2 = (char) (ch + ch2);
		char ch3 = 1+'a';
		int ch4 = 1+'a';
		System.out.println("ch3:"+ch3+"    ch4:"+ch4);//ch3:b    ch4:98
		System.out.println(ch2);//b
		
		int i1 = 1;
		int i2 = 2;
		i1 = i1 + i2;
		System.out.println(i1);//3
		
		short s =4;
		s = s+5;//编译失败
		s = (short)(s+5);//需要强转
		s+=5;//编译成功,做自动转化

	}
	public static void main(String[] args){
		Integer a = new Integer(3);//手动创建对象,不会引用常量池中的对象;
		Integer a2 = new Integer(0);//手动创建对象,不会引用常量池中的对象;
		Integer b = 3;//将3自动装箱成为Integer类型
		int c = 3;
		System.out.println(a==b);// false 两个引用没有引用同一个对象(易错)
		System.out.println(a==c);// true 自动拆箱然后在和C进行比较
		System.out.println(a+a2==c);//true java的数学计算实在内存栈里面,java会对a和a2进行拆箱操作
		
		Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
		System.out.println(f1==f2);//true
		System.out.println(f3==f4);//false
		//自动装箱时-128到127引用常量池中的对象,否则新建对象
	}

java常量池

添加链接描述
在这里插入图片描述

  • 程序计数器是jvm执行程序的流水线,存放一些跳转指令。

  • 本地方法栈是jvm调用操作系统方法所使用的栈。

  • 虚拟机栈是jvm执行java代码所使用的栈。

  • 方法区存放了一些常量、静态变量、类信息等,可以理解成class文件在内存中的存放位置。

  • 虚拟机堆是jvm执行java代码所使用的堆。

  Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
   所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
  类和接口的全限定名
  字段名称和描述符
  方法名称和描述符
   而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法
   String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

常量池的好处
   常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。

String与常量池

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
          
System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
System.out.println(s4 == s5);  // false
System.out.println(s1 == s6);  // true

  首先说明一点,在java 中,直接使用= =操作符比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。
   s1 = = s2这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。
  s1 = = s3这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = “Hel” + “lo”;在class文件中被优化成String s3 = “Hello”,所以s1 = = s3成立。只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中
  s1 == s4当然不相等,s4虽然也是拼接出来的,但new String(“lo”)这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,鬼知道s4被分配到哪去了,所以地址肯定不同。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中
在这里插入图片描述
  s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,不能在编译期被确定,所以不做优化,只能等到运行时,在堆中创建s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。
在这里插入图片描述
  s4 = = s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。
  s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。
特例:

public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
     String s = A + B;  // 将两个常量用+连接对s进行初始化 
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t,它们是同一个对象");   
     } else {   
         System.out.println("s不等于t,它们不是同一个对象");   
     }   
 } 
输出:s等于t,它们是同一个对象在这里插入代码片

  A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s=“ab”+“cd”;

public static final String A; // 常量A
public static final String B;    // 常量B
static {   
     A = "ab";   
     B = "cd";   
 }   
 public static void main(String[] args) {   
    // 将两个常量用+连接对s进行初始化   
     String s = A + B;   
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t,它们是同一个对象");   
     } else {   
         System.out.println("s不等于t,它们不是同一个对象");   
     }   
 } 
s不等于t,它们不是同一个对象

  A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。

结论:

  • 必须要关注编译期的行为,才能更好的理解常量池。
  • 运行时常量池中的常量,基本来源于各个class文件中的常量池。
  • 程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池。

猜你喜欢

转载自blog.csdn.net/weixin_43192732/article/details/85270236