第二章 Java数据类型之基本数据类型
前言
这篇文章是我关于Java数据类型的笔记分享,如有错误欢迎指正。
一、Java数据类型是什么
就像数学中会把数字划分为正数负数、小数整数、实数复数,代码世界中的数据也有他们各自的类型,每一种类型具有他自身的一定属性、定义、规范和功能等,各种变量、常量、方法等的定义都离不开数据类型。下面是我记录的Java数据类型的分类和使用。
二、Java数据类型分类模型
三、基本数据类型简介
基本数据类型的特性
基本数据类型是Java提供的内置类型。一般在定义变量、常量或者是作为方法返回值的时候使用。而在定义变量、常量的时候,虚拟机读到声明语句,会为定义的变量在内存中开辟一定大小的空间,存储不同的类型数据,这个空间大小就是数据类型决定的,而基础数据类型在类加载的时候就会被赋一个初始值,详细内容我记录在简析虚拟机一文。
byte 字节类型
一个byte类型占用8位空间,即1字节。是一种有符号整形,以二进制补码表示。
取值范围为[-128,128),默认值为0。
字节类型在一些数据量确定在[-128,128)范围内的整形数据处理中,替代最常用的int类型。因为一byte类型数据正好占用1字节空间,所以经常使用在字节流文件传输中。
public byte myByte = 1;
short 短整型
一个short类型数据占用16位空间,即2字节。是一种有符号整数,以二进制补码表示。
取值范围为[-32768,32768),默认值为0。
短整型介于byte和int之间,在运算数据确定不会超过其取值范围的时候,可以用short类型代替int类型来节省空间。
public short myShort = 1000;
int 整型
一个int类型数据占用32位空间,即4字节。是一种有符号整数,以二进制补码表示。
取值范围为[-2,147,483,648,2,147,483,648),默认值为0。
int类型是使用范围最广的数据类型,因为它能处理大部分整数类型的运算,是Java中整形的默认数据类型。比如一个123作为字面值,java会认为它是一个int类型而不是byte。
public int myInt = 1000000;
long 长整型
一个long类型数据占用64位空间,即8字节。是一种有符号整数,以二进制补码表示。
取值范围为[-263,263),默认值为0L。
long类型一般用于需要较大整数的情况下,如果不是特别需要尽量不使用long类型,因为它会开辟较大空间,容易造成资源浪费。需要注意的是,为了区分long类型和int类型,long类型整数在定义的时候,其字面值需要在结尾加上L或者l,这里不区分大小写,不过小写的l和数字1比较像,推荐使用L。需要指出一点,如果等号后面的值在[-2,147,483,648,2,147,483,648)范围内,即使末尾不加L也不会出现编译错误,但如果在这个范围外不加就会,这其实是因为不加L的整数字面值,编译器会把它理解为int类型,而在赋值的时候进行了隐式类型转换,详情下文会讲解。
public long myLong = 1000000000L;
public long myLong2 = 2147483647;
//public long myLong3 = 2147483648; 这样就会出现编译错误
float 单精度浮点型
一个float类型数据占用32位空间,即4字节。单精度、符合IEEE 754标准的浮点数。
IEEE 754标准将一串二进制内存划分为三个功能区, 其中s为符号位,f为尾数位,e为指数位,按s e f的顺序存储在内存中。
float各位位数分别是:s占1位,e占8位,f占23位。
默认值是0.0f。
和long类型的表示方法类似,在定义单精度float的时候需要在字面值末尾添加f或者F。浮点数因为存储和计算方法的问题,浮点数不能表示需要精确表示的值,比如货币。
public float myFloat = 1.1f;
double 双精度浮点型
一个float类型数据占用64位空间,即8字节。双精度、符合IEEE 754标准的浮点数。
double的各位位数:s占1位,e占11位,f占52位
默认值是0.0。
double 是浮点数的默认类型,就是浮点数字面值结尾如果不加f,Java默认这个字面是是double类型的,不过也可以在浮点数字面值结尾加上d来表示这个数是双精度类型的。
public double myDouble = 1.1;
public double myDouble2 = 1.1d;
char 字符类型
字符类型是一个单一的16位Unicode字符,和C++不同,C++的char是占8位空间的。
默认值为空字符’’,引号里面没有东西,是一个字符而不是Java提供的null,对它进行字符相关操作不会抛出空指针异常。
Java中对char类型的赋值方式有几种
- 引号括住单个字符
char myChar = 'A';
- 通过整数赋值,可以是十进制、八进制、十六进制数等等。
//以下赋值结果都为A
char myChar = 0b1000001;
char myChar1 = 65;
char myChar2 = 0101;
char myChar3 = 0x41;
boolean 布尔类型
布尔数据类型表示1位信息(并不代表它占用1位字节),代表0或者1,值作为标志记录true/false情况。
默认值是false。
布尔本身是Java提供的状态标识信息,但是使用打印方法将布尔类型的数据输出到控制台的时候,打印的内容会是true或者false两个单词。而且通过布尔类型,可以直接对流程控制语句进行操作而不用添加判断运算符。
boolean flag = true;
if(flag){//不用再使用flag==true来进行判断
//...
}
这里再注解一个知识点,上文提到过布尔类型的占用空间大小其实是不固定的,Java提供的八中基本数据类型中只有boolean没有确定这个属性。经过查阅得知,这是因为Java虚拟机(JVM)根本不认识boolean这个类型,布尔类型数据在编译之后会转换成其他数据类型存储。
Although the Java Virtual Machine defines a boolean type, it only
provides very limited support for it. There are no Java Virtual
Machine instructions solely dedicated to operations on boolean values.
Instead, expressions in the Java programming language that operate on
boolean values are compiled to use values of the Java Virtual Machine
int data type.
上文是jvm规范对boolean的介绍,翻译后解释就是,虚拟机定义了布尔类型,但是没有单独的虚拟机指令专门用于布尔值的操作,就是说虚拟机对布尔值提供的支持很少,更多情况下是将它转换为其他类型进行运算,所以布尔类型数据占用多少空间,主要看虚拟机实现方(上面只是规范,和具体实现还是不一样的)。
比如hotspot虚拟机,Java代码编译后,单个boolean类型数据会被编译成int类型,一个boolean数组会被编译为byte数组。而对于其他版本的虚拟机来说,实现方式也许又不一样。
ps: java字面值
字面值可以理解为,是程序员在赋值时手动输入的值,可以与变量的实际值对应理解。最直观的例子就是上面对char变量的赋值:
//以下赋值结果都为A
char myChar = 0b1000001;
char myChar1 = 65;
char myChar2 = 0101;
char myChar3 = 0x41;
像0b1000001、65等都是整形字面值,就是程序员输入的值,但是char变量实际存储的值是’A’。
字面值种类很多,大体上可以分为:整数型、浮点型、字符及字符串型和特殊字面值。
- 整数型字面值:例如1000,29L,0xFF这些都是整型字面值,表示一个整数。
- 浮点型字面值:如1.1,5.2F都是浮点型的字面值。
- java7之后,所有数值类型的字面值可以在数字之间加入下划线帮助阅读,但不能分割字符,如:
int x = 123_123_123;
x = 123___123;//也可以使用连续的下划线
double d = 1.2_3;
d = 1._23;//这就是不合法的状况
long l = 1234_L;//这也是不合法的状态
- 字符及字符串字面值:字符和字符串字面值区别于其他的字面值在于它需要用引号或者单引号包裹起来,如’1’就是字符字面值,1就是整型字面值。字符和字符串型字面值除了直接用字符+双引号表示之外,有些键盘上没有的转义字符,可以通过反斜杠+特殊字符表示。常见的有:\’单引号,\’’双引号,\反斜杠,\n换行符,\f换页符,\t制表符,\b回格符。
- 特殊字面值:java中特殊的字面值主要有两种:null和class literal(类名称字面常量)。
- null是一种特殊的字面值,可以赋给任何引用类型变量(不能赋值给基本类型),null详解建java关键字一文,null是java的关键字。
- class literal,用类型名+.class获取,这个字面值不是程序编写时输入的,而是java自带的一种常量。举个例子:
System.out.println(Integer.class);
//输出结果为 class java.lang.Integer
ps:IEEE 754标准
IEEE浮点表示法标准形式: V = (-1)s×M×2E
java中两种浮点类型的内存占用格式上文已经介绍过了,发现公式中有参数M、E,而内存表示的尾数位是用f指代,指数位用e指代,这并不是表示失误,而是因为f和M之间、e和E之间还有一个关系计算。下面进行举例解释。
首先先弄清楚二进制内存中的数据转换为公式的值之间的转换关系:
- 规格化的值:当指数位e中的值不全为0或1,说明这个值是一般规格的值,此时:M = 1+f;E=e-Bias,其中f是f所包含的bit组成的二进制小数,Bias=2k-1-1,k是e的bit数,如果浮点数值是32位float类型的,k就等于8,double类型k就等于11。计算之后M、E代入公式计算真正的浮点值
- 非规格化的值:当e中的bit值全为0,这个值就是非规格化的值。此时:M=f,E=e-Bias。
- 特殊值:当e值都为1,若f中的值全是0,代表无穷大;若f中的值不全是0,代表NaN,Not a Number。
例:
float f = 1.1f;
//将浮点类型转化为进制整数表示
int bit = Float.floatToIntBits(f);
//将bit转换为二进制整数并打印
System.out.println(Integer.toBinaryString(bit));
//结果: 11 1111 1000 1100 1100 1100 1100 1101
上面例子中,打印的结果总共30位,理论上float一共占用32位,这是因为1.1f在内存中占不到32位,只有31位,指数位的第一位为0省略,符号位为0也被省略,补上之后整理:
0 01111111 00011001100110011001101
这是规格化的值,用公式计算
尾数位表示的是二进制小数,即第一位代表1/2,第二位1/4,第三位1/8依次类推。
M=1+f= 1+1/24+1/25+1/28+1/29…+1/223=1.10000002384185791015625=1.1
E=e-Bias=127-127=0;
最后结果值=(-1)0x1.1x20=1.1。不过通过上面的计算可以发现,浮点类型终究需要一定的舍入规则才能的到想要的值,所以浮点类型是不精确的。
三、Java变量
java变量的含义
java变量是参与程序运算的基本单位之一,就跟数学里的变量一样,如一个数学方程y=x+1,这里的x和y就是变量,1可以看做常量,当然程序中也可以直接用整形变量来表示1,上面这个程序就可以写成
double x,y;
x = 2;
y = x + 1;
//输出y得到y的值
java变量的定义
java变量完整的定义需要三个元素:变量数据类型、变量名、变量值。
- 变量数据类型,可以是java八大基本数据类型,也可以是引用类型,引用类型一般就是类、接口、数组类型,数据类型是变量定义的时候必不可少的,因为它规定了这个变量的基本属性:变量允许的值类型、变量需要开辟多大内存空间等都有数据类型决定。如上面例子中的double就是变量x、y的数据类型。
- 变量名,变量名是这个变量唯一的标识符,在同一个作用域内,不能两次定义同一个变量名的变量,变量名是java标识符的一种,所以它的命名需要满足标识符的命名规则,并且采用小驼峰命名规范。如上面例子中的x、y就是变量名,两者不能同名,即不能定义成double x,x;
- 变量值,可以是变量在声明时程序员通过赋值运算符=直接赋给变量的字面量,也可以是在程序执行过程中通过运算得到某个值赋值给变量。如果整个程序中都没有对变量进行赋值,那么java在类装载的时候会给基础数据类型的成员变量进行赋默认值(引用类型和其他作用范围的变量不会)。比如上面那个例子,x=2就是显式的给x变量赋值,而y=x+1是通过运算之后给y赋值,一般情况下赋值都需要通过赋值运算符“=”执行。
java变量的作用范围
- java变量作用范围的划分:类级(静态变量)>对象实例级(成员变量)>方法级(方法中的局部变量)>块级(代码块中定义的局部变量)。
- 类级:即静态变量,和成员变量在一个地方定义,通过static修饰,隶属于类,即每个该类对象共享一个静态变量的空间。静态变量在类加载的过程中就存进JVM内存并赋值,如果是基本数据类型,就算程序中没有显式的对其赋值,类加载时也会给它一个默认值,而引用类型静态变量则会是一个null,java程序中调用null可能会出现空指针异常。静态变量的生命周期和整个程序运行时间相同。关于java静态变量的内容我单独记录了一篇博客。
private static int myStaticInt = 10;
- 对象实例级:就是最常见的成员变量,代表类的抽象属性,每个对象有一个属于对象实例自己的具体属性,比如人这个类,就拥有身高这个属性,每个人类对象,也就是个人的身高都是不同的,这个属性属于个人。成员变量在对象生成的时候才开辟空间,如果在程序中没有显式的对成员变量赋值,那么基本数据类型的成员变量也拥有默认值,引用类型的成员变量则没有。成员变量的生命周期和它隶属的对象生命周期相同。
- PS:就是虽然静态变量和成员变量的生命周期不同,存储位置不同,但是两者在声明是在同一个域里的,所以两者不能声明同名变量。
public int myInt = 10;
//这个时候再声明一个 public static int myInt = 1;会出现编译错误。
- 方法级:即方法中定义的局部变量,方法中定义变量,不能有控制访问符(public/protected/private)。同一个域里面不能定义同名变量,局部变量因为是在类中定义,所以可以定义和成员变量或静态变量同名的局部变量,程序在调用时会优先调用层级比较低的那个(局部变量)。局部变量在方法被调用的时候才会创建,并且不能声明为static,所有类型的局部变量都没有默认值。需要注意的是,方法的形参属于方法的局部变量。
public int a = 1;
public void function(){
int a = 10;
System.out.println(a);//打印的结果为10而不是1
}
- 块级:代码块中的局部变量,代码块可以嘶构造代码块,循环代码块,判断代码块等等。它和方法级局部变量类似,生命周期就是代码块的执行周期,比如一个循环代码块,循环执行结束之后局部变量就销毁,如下面例子中了i和temp,在for循环之外是无法调用的。
public void function(){
int a = 1;
for(int i=0;i<10;i++){
a = a + i;
int temp = a;
}
}
四、基本数据类型的存储
Java程序的运行是基于Java虚拟机(JVM)的,而程序在虚拟机中运行时,肯定要占用虚拟机的内存来存储类、变量等运算所需的基本信息,下面是我理解的java基本数据类型变量的存储。
4.1、java类的加载机制和JVM内存分配
了解基本数据类型的存储之前,有必要先简单了解一下,java的类在从高级语言被编译解释成机器语言的过程,以及这些数据在java虚拟机(JVM)中是如何存储的。
虚拟机内存模型
虚拟机中各个内存空间具体是什么功能,存储哪些数据在另一篇文章中记录,这篇文章简单介绍一下虚拟机栈和堆。
- 上图中红色部分都是线程私有的,换一种说法是,一个类对象会拥有自己的虚拟机栈、本地方法栈和程序计数器。简单点理解,虚拟机栈中存储的就是类对象的各种信息。
- 上图中绿色部分是线程公有的,也就是说多个对象可能会共享堆或者方法区中的数据内容,在一个对象中对堆数据做出更改,另一个对象也能看到这个变化。
- ps:这里把堆和方法区贴着画在了一起,是因为虽然在逻辑上给了他们俩不同的名字,但其实上两者都是属于一片堆空间,像堆中区分出了新生代和老年代一样,元空间(原本叫永久代)可以理解为一个特别的堆年代。
- 方法区和永久代或元空间的区别:方法区其实是jvm规范提供的一个名称,不同的虚拟机供应商有各自的虚拟机实现,只要符合jvm规范,每个人都可以自己去实现一个java虚拟机,方法区就是规范中的内容之一,平时开发中最常用的hotspot虚拟机,它对方法区这个规范的实现就是永久代,后来更新为元空间,所以他们是一种规范和实现的关系。
类加载简介
java程序在执行时,编译器会将.java文件编译成一种.class文件,然后java虚拟机对class文件进行解释执行,这也是java具有良好平台性的原因,有class文件和jvm虚拟机存在,就可以实现“一次编译,到处运行”。其他面向对象像C++,它会直接调用操作系统提供的方法控制资源,如果将代码移植到另一个操作系统上,就容易出现问题。
java在编译时,创建一个.class文件,这个文件中包含了一些编译期就确定下来的数据。然后虚拟机读取.class文件,将其中存储的数据装载到内存中。虚拟机加载类的步骤大致上为:加载-连接-初始化。
- 加载就是加载.class文件,进行数据读取。
- 连接包括:验证,确保被加载的类的正确性。准备,为静态变量分配内存并赋默认值。解析:把符号引用转化为直接引用,一些方法的调用就是在这一步确定。
- 初始化就是给类的静态变量赋正确的初始值。
在执行完上述步骤后,jvm会创建一个类的Class对象实例,这个对象存储了类的相关信息。
4.2、不同级别基本类型存储方式
一般情况下,所有基本数据类型都是存储在栈中的,但是也有特例,不同作用域的基本类型变量的存储方式也会有所区别。基本数据类型的名和值一般是一起存储的,引用类型名和实际值可能就存储在两个不同的内存中
- 静态变量:(基本数据类型)静态变量被static修饰,和全局变量在一个地方声明,声明的静态变量可以理解为属于类,类的各个对象公用同一个静态变量。静态变量在编译时就被确定,其值直接保存在.class文件中,在虚拟机加载类的时候读取class文件的数据直接初始化,Class实例保存在堆(元空间)中,在编译之后被创建,每个类唯一存在。
- 全局变量:即对象级变量,属于每个实例。全局基本数据变量,在加载类之后保存在堆空间的常量池中,在有新对象被创建时,从常量池中复制到对象的所属栈中。
- 局部变量:每个对象有一个属于自己的方法栈,当一个方法被调用时,会压入一个栈帧,栈帧是一种数据结构,其中包括了调用的方法引用、局部变量等信息,方法中声明的局部变量就存储在栈帧中,当方法调用完毕,栈帧被销毁,其中的局部变量自然就销毁了。
五、基本数据类型(除boolean)的相互转换
布尔类型不参与数据类型转换
5.1 显式类型转换
顾名思义,显式类型转化就是手动在数据的字面值前加上(想要转化为的数据类型),例如:
int myInt = 1;
short i2s = (shrot)myInt;//这里如果不加short进行显式转换会出现编译错误
显式类型转化一般用于编译器不能自动进行类型转换,或者用于标识清楚数据类型的时候使用。
5.2 隐式类型转换
java隐式类型转化基于一定规则:
- 同一种大类中,占用内存多的可以直接转换为占用内存少的
- 整数类型都可以隐式转化为浮点类型
- char类型可以转化为int但是不能转化为short。
- 类型转化具有传递性
如图:
- 箭头方向表示允许进行隐式类型转换的方向
- 类型转换可以传递的意思是,如果int可以转换为long,short可以转换为int,那么short也可以转换为long。可以把上面的图看做有向图,路径可达就代表可以进行隐式转换。
- 整形转换为浮点型容易出现精度丢失,这是因为两者计算方法不同导致。
这里举例进行解释
byte myByte =1;
short myShort = myByte;
int myInt = myShort;
char myChar = 'c';
myInt = myChar;
//因为int类型可以隐式转换成long类型,所以long类型赋值时数字末尾不加L也不会编译异常
long myLong = myInt;
//float类型占用32位内存,long类型有64位内存,但是可以进行转换
float myFloat = myLong;
double myDouble = myFloat;
上面就是几种数据类型的隐式转换,两两之间转化不需要添加(数据类型),编译器会自动分析等号左边的数据类型,然后把等号右边的类型强制转换成左边的。
ps:上面有几点细节
- long类型占用64位内存,而float只有32位但是编译器允许他们之前进行相互转换,这是因为浮点型的计算方式和整形不同,并不是按位读取,而是把内存分为三个分区计算,所以float可以表示的数的字面值比long还要多。
- 字符类型转换为整数之后,整数值是字符对应的ASCII码。
总结
这篇文章是我学习java基本数据类型的笔记。其中有基本数据类型的简介(附带有java字面值的含义和IEEE标准计算浮点数的方法)、java变量、java基本类型变量的存储以及基本类型之间的转换,如果有错误欢迎指出。