8大基本数据类型和引用类型知识点总结
2020年04月15日23:42:28更新:修改的3.1的错误和图(只有运行时常量池在jdk1.7时移到堆中)。
2020年4月14日23:00:03更新:加入了两道笔试时做错的题目(String相关)和相关知识内容(3.2第五点,3.4)。
2020年8月11日16:39:32更新:新增了float和double的详细内容(在另一篇文章,这里只给出跳转链接)。
前言
Java中有且只有两种数据类型,一种是基本的数据类型int,double等,除此之外的都是引用数据类型。
1. 八大基本数据类型
- 整型(byte、short、int、long)
- 字符型(char)
- 浮点型(float、double)
- 布尔型(boolean)
接下来介绍一下不同类型的所占内存。介绍之前先回顾一下计量单位。
- 位(bit):最小的信息单位,计算机中所有的数据存储时都是由二进制数来保存,1位代表一个二进制数。
- 字节(byte):常用的信息单位,每1byte = 8bit = 8个二进制数。
举例:int的取值范围是 -231到231-1,大家都知道他是由32位二进制数组成,所以就是32/8 = 4字节。
数据类型 | 占用字节/占用位 |
byte | 1/8 |
short | 2/16 |
int | 4/32 |
long | 8/64 |
double | 8/64 |
float | 4/32 |
char | 2/16 |
boolean | 1/8 |
1.1 强制类型转换
当不同基本类型的对象要进行运算的时候,都会先进行强制类型转换,转成同一类型后再进行计算,且得出的结果也是强制类型转换后的结果。
强制类型转换的优先级如下:
箭头表示可以强制转换成该类型,比如说byte可以强制转换成short类型。
虚线表示在转换类型的时候可能会发生数据丢失的情况。
double a = 3.2;
int b = 4;
System.out.println(a + b); // 7.2,编译器不会报错。
1.2 float和double(浮点数)
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a); // 0.100000024
System.out.println(b); // 0.099999964
System.out.println(a == b); // false
double c = 2.0;
double d = 1.1;
System.out.println(c-d); // 0.8999999999999999
答案和我们的常识并不相符,首先要先来看一下浮点数是如何存储的。
float(共32bit) | 符号位(1bit) | 指数位(8bit) | 有效数字(23bit) |
double(共64bit) | 符号位(1bit) | 指数位(11bit) | 有效数字(52bit) |
它们具体是使用IEEE754的标准进行数据的存储,因此无论是float还是有效数字更多的double都不能精确表示一个数,如果要精确表示数字的话(比如货币)和数字比较的话,用decimal类型。
这里只是简单介绍一下double和float,如果还想知道关于浮点数更加具体的内容,可以参考这篇文章。
为什么在处理金额时不能用Double和Float,计算机是如何存储浮点数的
https://blog.csdn.net/qq_41872247/article/details/107861608
1.3 char
先介绍一下Unicode字符集,简单来说这是一张表格,为全世界每种语言的每个字符设立了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
然后Unicode最高是支持4字节的字符处理,也就是说无论是哪个国家的哪个字符都可以在32个二进制数以内表示。
正好我们的Java语言中,是采用Unicode字符集,char的长度是2字节。
-
Q:为什么char明明是2字节,可以存储汉字?
A:在unicode中,所有汉字在两个字节的范围内,都可以用2字节表示。 -
Q:如果用char存3字节的字符会怎么样(UTF-8编码模式下)?
A:存不进去,会报错。下面举例子来说明。
例:汉字 ‘风’ 在unicode表中的编码号是 2EDB(16进制,一个数就代表4个二进制数,所以是16位)。
在编译器中,可以通过\uXXXX(X代表16进制数)直接以unicode编码方式存入数据。char c ='\u2EDB'; System.out.println(c); // 风
那么,存一个3字节的数会怎么样呢?
可以看到,\u101 3 是蓝色的字色,这就表示编译器将其认成了字符,而后面多出来的3则是绿色的字色,就并没有被包括在字符内。
刚刚是我并且当我复制这个字符,在编译器中粘贴的时候,会自动变成这个情况(因为是采用了UTF-8编码),也是不能过编译。
-
Q:那我无论如何都想存该怎么办?
A:可以用String类型来存储,String采用了UTF-8的编码方式(中文汉字和中文符号用3字节存储,英文用1字节存储,动态长度,可以转成byte数组),可以过编译。String s = "\uD800\uDD33"; System.out.println(s); // 𐄳(如果看不见这个字符检查一下浏览器的编码格式)
1.4 boolean
boolean在内存中到底占几个字节,其实并不是一个确定的值,它取决于你的JVM的情况。
首先,boolean是只需要一个二进制数就可以表示的类型,但是一般在处理数据的时候最小单位不会用bit而会用byte,所以boolean至少是占用1字节的。
而具体占用多少字节并不清楚,有说是boolean在编译后等同int占4字节,boolean数组等同byte数组占1字节,也有说就只占1字节的。说法较多,但是可以肯定是一定是根据你的JVM来决定,JVM的规格会决定boolean占多少字节。
Java中boolean类型到底占用多少个字节?_gaoyong_stone的博客-CSDN博客
https://blog.csdn.net/gaoyong_stone/article/details/79538195
Java中boolean到底占几个字节_Jeff的专栏-CSDN博客
https://blog.csdn.net/jeffhtlee/article/details/7839377
1.5 包装类型和基本类型
每种基本数据类型都会有一个对应的包装器类型,比如int就对应Integer,double就对应Double,相当于给基本类型披了一个马甲,让基本类型也能变成引用类型。两者的具体区别如下:
- 包装类型可以为 null,而基本类型不可以。
//编译可以通过,但是会报空指针异常 Integer a = null; int b = a; // 下面的代码无法通过编译 // int a = null
- 包装类型可以用泛型,基本类型不行。
// List<int> array = new ArrayList<>(); // 报错 List<Integer> array = new ArrayList<>(); // 正常编译。
- 基本类型更加高效, 这个不用多说,基本类型是直接从栈中取数据,而包装类型则是一个类,要从堆中读取数据。
- 包装类型可以值相同,但==却是false。
int a = 1; int b = 1; System.out.println(a == b); // true // 我们一般不用这种方式创建包装类,因为包装类有自动装箱和拆箱,下面有写 Integer c = new Integer(a); Integer d = new Integer(a); System.out.println(c == d); // false,本质上是两个对象的地址比较,地址不同为false
- 包装类型和基本类型直接可以通过自动装箱和拆箱进行相互转换 。
顺便一提,由于自动装箱和自动拆箱的存在(包装器类型和基本类型可以互相转换),会出现这样的问题。// 自动装箱 int a = 1; Integer a1 = a; // 本质上是 执行Integer a1 = Integer.valueOf(a); // 自动拆箱 Integer b = 2; int b1 = b; // 本质上是 执行int b1 = b.intValue();
2. 引用数据类型
除了基本类型以外的数据都是引用数据类型。我们常说的对象也是引用数据类型,Java在使用引用类型和基本类型的时候有个决定性的区别就是值传递和引用传递。
- 每个引用类型占4字节。
- 引用类型存放的数据代表着数组所在的地址。
2.1 值传递和引用传递
// 值传递
public static void main(String[] args) {
int a = 3;
func(a);
System.out.println(a); // 3
}
public static void func(int i){
i = i + 10;
}
// 引用传递
// 还是一个int,做一个简单的封装
class Test{
int a;
}
public class Main {
public static void main(String[] args) {
Test t = new Test();
t.a = 3;
func(t);
System.out.println(t.a); // 13
}
public static void func(Test i){
i.a = i.a + 10;
}
}
想要介绍说清值传递和引用传递的区别,就要先弄清Java对于基本数据类型和引用数据类型的存储方式。
2.2 JVM中堆与栈
JVM使用栈(stack)和堆(heap)来进行数据的存储,所以接下来说的是程序内存中的堆和栈,并不是数据结构的堆和栈。
区别 | 堆内存(heap) | 栈内存(stack) |
存储对象 | 各种局部变量和方法帧 | 对象和数组 (数组也是对象的一种) |
优点 | 动态分配内存 | 存储速度快 |
缺点 | 存储速度慢 | 存储空间不大 |
线程共享 | JVM中所有线程共享 | 不在多线程内共享 所以每个线程都有一个栈 |
下面举两个例子来说明堆和栈是怎么工作的:
- 基本数据类型:
public class Test { public static void main(String[] args) { int a = 10; func(a); System.out.println(a); // 10 } public static void func(int i){ i = i + 10; } }
- 首先将main方法加入到栈内存中。
- 基本数据类型都存放在栈内存中,所以直接给数据a赋值10。
- 然后将func方法加入到栈内存中,再给参数 i 赋值10,这里就可以看出来,无论func中的i怎么改变,都不会影响到main中的a。
- 引用数据类型:
public class Test { int a; int b; public static void main(String[] args) { Test t = new Test(); func(t); t = null; } public static void func(Test i){ i.a = 10; } }
- 首先将main函数加入到栈内存中。
- 在执行到Test t = new Test()这一句的时候,JVM首先会在堆内存中,创建这个对象的实体(包括a和b两个数据,也都放在内堆存中)。
- 然后再给这个实体对象分配一个内存地址(这里假设是0x11,实际上地址不一定),通过这个地址就可以访问到这个对象。
- 接下来,把这个地址赋值给t,这样我们就可以通过t来访问这个Test对象的实体了。
- 再看func(t)中,先将func方法加入到栈内存中,再给参数 i 赋上 t 的值(0x11),这里就可以看出,两个方法操作的对象是同一对象了。
- 然后再执行t = null,那么t就不再具有访问Test对象的功能,但是Test对象并没有销毁,而销毁的时间看JVM的垃圾回收机制。
3. 特殊的引用类型String
String 这个类比较特别,因为它既可以用双引号“”直接赋值,也可以用new String()来创建对象。那么,不是包装类的String为何可以用两种方式创建对象呢?
接下来要从2中的堆和栈的知识点中再添加一个新内容:字符串常量池。
3.1 字符串常量池
JVM中有有一个叫做常量池的东西。
常量池分为静态常量池和运行时常量池,其中静态常量池就是CLASS文件中的常量池,存在CLASS文件中,而运行时常量池就是JVM在运行的时候,用来存放一些常量数据。
运行时常量池在JDK7之前存放在方法区中,JDK7开始改为存放在堆内存中。
而在这其中,字符串常量池就是运行时常量池中的一部分,主要就是要注意这一部分即可。
3.2 String和常量池的关系
-
当用""直接赋值给String的时候,首先会在字符串常量池中寻找是否有这个字符串,如果有,就将这个字符串的地址赋值给String。如果没有,则会新建一个字符串,将其放到字符串常量池中,再返回它的引用给String。
String str1 = "1"; String str2 = "1"; System.out.println(str1 == str2); // true,两个字符串指向同一个字符串常量。
-
就算用 + 将字符串分割成多部分,1的结果不会改变,这是Java特有的优化机制。
String str1 = "11"; String str2 = "1" + "1"; System.out.println(str1 == str2); // true, 仍然指向同一地址。
-
用new String的方式创建对象的时候,还是会和1和2一样,从字符串常量池中寻找是否有这个字符,无论有没有都会新建这个字符串,再将这个字符串复制到堆内存中,最后将堆内存的副本的地址返回给String对象。
String str1 = "1"; String str2 = new String("1"); System.out.println(str1 == str2); // false, 一个指向常量池,另一个指向堆内存。
-
在赋值的时候,如果同时出现了new String和 " " 时,会用上StringBuilder这个类来进行辅助创建字符串对象。
String a = "1"; String b = "3"; String str1 = "123"; String str2 = "1" + new String("2") + "3"; // 本质String str2 = new StringBuilder("1").append(new String("2")).append("3").toString() System.out.println(str1 == str2); // false, 一个指向常量池,一个指向堆内存
-
在赋值的时候,如果是如下图所示的创建对象,会先用上反射来创建一个StringBuilder,然后再通过StringBuilder创建字符串。
String a = "1"; String b = "2"; String str1 = "12"; String str2 = a + b; // 先用反射创建一个StringBuilder,剩下的同4。 System.out.println(str1 == str2); // false, 一个指向常量池,一个指向堆内存
3.3 StringBuilder和StringBuffer
String本身是一个不可变类,也就是说,当创建一个String了之后,它的内容就会生成在常量池中,不会再变动了。如果你重新赋值一个新的值给String,也只是在常量池中再创建一个新字符串。原来的赋值并不会消失。
String str1 = "1";
str1 = "12";
而StringBuffer就是一个可以变动的字符串,可以对字符串做出操作。
StringBuffer str1 = new StringBuffer("1");
str1.append("2");
最后说一下StringBuilder,StringBuilder就是StringBuffer的高效率非线程同步版。
类 | 描述 |
String | 字符串常量,创建之后不可更改 |
StringBuffer | 字符串变量,创建之后可以修改 有线程同步的特性,大部分方法用synchronized修饰 |
StringBuilder | StringBuffer的非线程同步版 大部分方法没有用synchronized修饰,所以效率高 |
3.4 有关StringBuilder和String的equals方法
-
StringBuilder没有对equals方法进行重写,也就是说StringBuilder的equals方法等同于Object的equals方法。
-
而String的equals方法只有当目标类能够是String类的实例时,才能进行比较。而StringBuffer和StringBuilder并没有继承String类。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc { ...... public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { // 判断目标类是否能是String类的实例 String aString = (String)anObject; if (coder() == aString.coder()) { return isLatin1() ? StringLatin1.equals(value, aString.value) : StringUTF16.equals(value, aString.value); } } return false; } ...... }
所以,请回答下面代码的输出内容:
String str1 = "aaa";
StringBuffer str2 = new StringBuffer(str1);
StringBuffer str3 = new StringBuffer("aaa");
System.out.println(str1.equals(str2));
System.out.println(str3.equals("aaa"));
System.out.println(str3.equals(str1);
System.out.println(str3.toString().equals("aaa"));
第一行str2是StringBuffer类,不能转成String类,false。
第二行StringBuffer类的equals方法等同Object的方法,不是同一对象,false。
第三行同理,不是同一对象,false。
第四行,aaa可以转化为String类,对比后数值相等,true。
参考材料
彻底理解Java中的基本数据类型转换(自动、强制、提升) - Java技术栈 - 博客园
https://www.cnblogs.com/javastack/p/9111750.html
java的char类型只有两个字节为什么可以存储汉字?-阿里云开发者社区
https://developer.aliyun.com/ask/65417?spm=a2c6h.13159736
Java中 float、double使用注意问题 - panda521 - 博客园
https://www.cnblogs.com/chenjfblog/p/7737332.html
面试官:兄弟,说说基本类型和包装类型的区别吧_沉默王二-CSDN博客
https://blog.csdn.net/qing_gee/article/details/101670051
堆和栈的概念和区别_pt666的博客-CSDN博客
https://blog.csdn.net/pt666/article/details/70876410/
深入理解Java中的String(大坑)_String,java_我的书包哪里去了-CSDN博客
https://blog.csdn.net/qq_34490018/article/details/82110578
String、StringBuffer和StringBuilder的区别_幸遇三杯酒好,况逢一朵花新-CSDN博客
https://blog.csdn.net/csxypr/article/details/92378336