Java数据类型
Java数据类型分为基本类型与引用类型,这里先介绍基本类型。基本类型也分为2类:数值类型和布尔类型。
基本类型
数值类型
整数类型
Java规定补码表示整数类型,因此一个w位的整型,其范围为:[ − 2 w − 1 -2^{w-1} −2w−1, 2 w − 1 − 1 2^{w-1}-1 2w−1−1],整数类型有以下四种
byte类型
byte,中文意思为字节,即该类型占据8位的内存空间。
short类型
占据2个字节的内存,也就是16位。
int类型
占据4个字节,32位
long型
占据8个字节,64位
tips:
- int是最常用的整数类型,因此,直接给出一个整数,系统会默认其是int类型,如果该数范围超过了int所表示的范围,则会报错。
- 将一个小范围的整数(short或者byte范围内)赋给一个byte或者short类型的变量,则系统会将这个整数当作byte或者short类型来处理
- 如果希望使用一个巨大的整数值(long型),则应该在该数后面加上一个L或者l,但通常都加大写的L以避免混淆。
- Java使用补码表示整数,注意各个类型的第一位均为符号位,尤其是用其他进制赋值时注意。
整数类型的表示
Java中有4种整数表示方式:十进制,二进制(0b),8进制(0),16进制(0x)
int a = 0x87654321;
long b = 0x87654321L;
System.out.println(a); //-2023406815,因为不管用哪种进制表示整数,java都默认是int型,32位,故第一位默认都是符号位,因此是一个负值,且需要将补码转换得出结果
System.out.println(b); //2271560481,这时候为正了,这是由于在后面加了L,变成了long类型,而long型默认占64位,第一位才是符号位,赋值只有32位,因此这时为正。
字符类型
字符型用于表示单个的字符,使用单引号括起来。Java使用16位(支持65536个字符)的Unicode字符集作为编码方式,因此Java支持各种语言的字符。
所谓字符集就是个字符编号,一个字符对应一个二进制码。
字符表示形式
- 可以使用16进制编码表示字符,范围是’\u0000’-‘\uFFFF’,一共可表示65536个字符,其前256个字符与ASCII(占据一个字节)编码一致。
- 计算机底层保存字符时,实际上就是保存字符编号。因此char类型的值可以直接作为整型值来使用,相当于一个16位的无符号整数,范围是0-65535。
- 如果把0-65535间的整数赋给char型变量,则系统会自动将这个int整数当作char型来处理
- 同样,如果将一个字符赋给整型变量,则是将该字符对应的编号赋给变量。
- 如果将不在0-65535的值赋给字符型时,会报类型不匹配的错误。
- 同样,将一个字符赋给较小整型变量时,也会报类型不匹配的错误。
- 如果原始字符串中出现了单引号,双引号,\。则应该使用\去进行转义。
/*
在Java中,反斜杠\是具有特殊意义的,就是转义字符。
转义字符就是改变原有字符的意义,比如‘\t’表示制表符,是由于
转义字符转义r的意思而来的。
*/
char a = '\\'; //要想输出正常的反斜杠,则需要在前面再加一个反斜杠,使得原来的反斜杠没有“转义”的意义,变为正常的反斜杠
System.out.print(a);
char b = '\t';
System.out.print(b); //输出制表符
System.out.println(a);
char c = 97; //会自动将编号转换为对应的字符
System.out.println(c); // a
int d = 'a'; //自动转换为该字符的编号赋给d
System.out.println(d); //97
浮点类型
Java中一共两种浮点型:float(单精度),double(双精度)。分别占4个字节与8个字节。且在Java中浮点型默认是double型,若要表示为float型,则在后面加个f或者F即可。
Java还提供3个特殊的浮点值,正无穷大,负无穷大,NaN(非数)。
- 正无穷大:Float.POSITIVE_INFINITY或者Double.POSITIVE_INFINITY
- 负无穷大:Float.NEGATIVE_INFINITY或者Double.NEGATIVE_INFINITY
- NaN:Float.NaN或者Double.NaN
- 所有的正无穷都相等,所有的负无穷都相等,非值不与任何数值相等。非值之间也不相等。
float af = 5.2345464676f;
System.out.println(af); //精度不够,会让值发生变化
double a = Double.NEGATIVE_INFINITY;
float b = Float.NEGATIVE_INFINITY;
System.out.println(a==b); // 所有的负无穷是相等的
double c = 0.0;
System.out.println(c / c);// 0.0/0.0将得到非数
System.out.println(c/c == Float.NaN); // 非值之间不等
System.out.println(Double.POSITIVE_INFINITY == Float.POSITIVE_INFINITY);// 所有正无穷大相等
// System.out.println(0 /0); //将抛出除以0的异常
只有浮点数之间的运算才可能产生上述三种特殊值,整型之间则会报错。
布尔类型
Java语言中,布尔类型的值只能是true或者false,不能用0或者非0来代替。其他基本类型的值也不能转换为布尔型。Java中并没有强制指定布尔类型所占的空间。虽然实际上只需要一位即可,但通常是采用一个字节来存储。
使用var定义变量
使用var定义变量实际上是一种偷懒行为,这种情况下必须给变量赋初值,由编译器来推断变量的类型。这时实际上变量依然有确定的数据类型。
基本类型的类型转换
自动类型转换如下图
在Java中,给指定类型变量赋值时,只要这个值在其范围内,那么不用管什么默认不默认,这个值就是变量类型。如果不在指定范围且不发生自动转换,就会报错。但有些情况也是可以强制类型转换的,比如下面的最后3行代码
int a = 6;
float b = a;
System.out.println(a); //int自动转为float
byte c = 97;
// char d = c; 会报错,类型不匹配
char d = (char)c; //强制类型转换
System.out.println(d);// a
基本类型与字符串的转换
String s = 5.0 + "fdcj"; // 5.0fdcj
System.out.println(s); //会将基本数据类型自动转换为字符串
//注意下面两例的不同:
//第一个是先加再转
//第二个由于字符串在前,因此提前已经转换为了字符串,因此后面一个也不是加,也是字符串连接符。
System.out.println(3 + 4 + "hello!"); // 7hello!
System.out.println( "hello!"+ 3 + 4 ); // hello!34
强制类型转换
自动类型转换与强制类型转换都有一个前提,就是二者之间真的可以转换。例如字符串转换为整型是基本上不可能发生的。之所以要分强不强制,是因为在强制类型转换时有可能会发生溢出,精度缺失等情况,实际上是在提醒程序员你在做什么。自动转换,如上图,是不会发生什么问题的。也有可能将字符串类型转换为整型:当字符串内容全是数字时:
String s1 = "43";
int a = Integer.parseInt(s1);
System.out.println(a);
String s2 = "4/2";
int b = Integer.parseInt(s2); //报错
这个时候可以用包装类实现。
表达式类型的自动提升
**当一个算术表达式中包含多个基本类型时,整个算术表达式的数据类型将发生自动提升。**有如下规则:
- 所有的byte,short,和char类型将被提升至int类型。
- 整个算术表达式的数据类型将自动提升到与表达式中最高等级操作数同样的类型。
不用多想,记住即可!
常见的错误示例:
short s = 5;
short s1 = s + 2; // 右端最高级是int型,而左边数据类型是short,因此会报错,不过可以通过强制类型转换来解决。
下面是一些正确示例:
byte b = 40;
char c = 'a';
int i =23;
double d = .314;
double result = b + c + i*d; // 表达式数据类型总是保持与右端最高级数据类型一致
System.out.println(result);
对于整数除整数,结果也是整数:
short s = 567;
int e = 345;
int f = s / e;
System.out.println(f); // 1,与右端最高级(int)保持一致,是通过截断小数部分实现的。
当表达式中有字符串时,会有些不一样,因为字符串与基本类型之间的加号是字符串连接符,而基本类型之间的则是算术运算符。如下:
System.out.println("hello!" + 'a' + 7); // hello!a7
System.out.println( 'a' + 7 + "hello!"); // 104hello!
Java中也有常量池,如下:
String s1 = "hello";
String s2 = "hello";
String s3 = "he" + "llo";
System.out.println(s1 == s2); //true
System.out.println(s1 == s3); //true
对于字符串常量,Java会确保只有一个副本,因此s1,s2,s3均指向同一个副本,因此相等。
Java运算符
只罗列几个比较重要的和自己不熟的:
- 除法:除法的运算规则其实在前面已经说过了,这里再用代码简单演示一下。
var a = 5.2;
var b = 3.1;
var div = a / b;
System.out.println(div);// 1.6774193548387097
System.out.println("5除以0.0的结果是:" + 5 / 0.0); // Infinity
System.out.println("-5除以0.0的结果是:" + -5 / 0.0); // -Infinity
//System.out.println("-5除以0.0的结果是:" + -5 / 0);//将出现除0异常
主要就是整数相除时,除数不能为0,为0会引发异常,而对于浮点数,则允许除数为0,会产生正无穷或者负无穷。
- 取余:取余也是会有除法的,和除法一样,操作数都是整数时,除数不能为0。当操作数有1个或者都是0时,则允许除数为0,但是结果是非数(NaN)。0或者0.0对任何非0数取余都是0。
var a = 5.2;
var b = 3.1;
var div = a % b;
System.out.println(div);// 2.1
System.out.println("5对0.0取余的结果是:" + 5 % 0.0); // NaN
System.out.println("-5对0.0取余的结果是:" + -5 % 0.0); // NaN
//System.out.println("-5除以0.0的结果是:" + -5 % 0);//将出现除0异常
-
位运算符:这里主要介绍左移与右移
1. <<:左移运算,将操作数的二进制码整体左移指定位数,右边空出来的位用0填充
2. >>:右移运算, 将操作数的二进制码整体右移指定位数,左边空出来的位用原来的符号位补充。原来是正数则用0补充,原来是负数则用1补充。
3. >>>:无符号右移,右移指定位数后,左边总是用0补充。 -
位运算符遵循的规则:
1. 对于short和byte类型的变量,首先自动转换为int型再进行移位
2. 对于int类型变量,如果移动位数超过了32位,则真正的移动位数是对32取余之后的数
3. 对于long型变量,如果移动位数超过了64位,则真正的移动位数是对64取余之后的数
4. 移位运算并不会改变操作数本身,会生成一个新的运算结果 -
扩展运算符,这里不再赘述,只是记住一点,能使用扩展运算符,则尽量使用扩展运算符。这样的程序具有更好的性能以及更加健壮。
-
比较运算符:==与!=,看起来很简单,但是依然有一些值得注意的地方。
1. 基本类型与引用类型之间不能使用比较运算符,
2. 布尔类型不能与其他任何类型比较。
3. 对于引用类型之间的比较,比较的是是否指向同一对象,就是说指针的地址是否一致。
System.out.println("97与'a'是否相等: " + (97 == 'a')); //相等,之前反复提到,字符型实际上就是整数类型,只要数值相等,就对
var t1 = new BitCaculator();
var t2 = new BitCaculator();
System.out.println("t1是否等于t2: " +(t1 == t2));// 不等,因为二者是不同的对象。地址不一样
var t3 = t1;
System.out.println("t1是否等于t3: " +(t1 == t3)); // 相等,因为二者指向同一个对象。
- 逻辑运算符:其实也很简单,但是需要指出逻辑或,逻辑与,以及短路或和短路与的区别:
var a = 5;
var b = 10;
if(a > 4 || b++ > 10){ // 短路或,一旦遇到true则不再判断后面表达式的值
System.out.println("a的值为: " + a + ", b的值为: " + b); // a的值为: 5, b的值为: 10
}
var a = 5;
var b = 10;
if(a > 4 | b++ > 10){ // 不短路或,要全部计算表达式
System.out.println("a的值为: " + a + ", b的值为: " + b); // a的值为: 5, b的值为: 11
}
短路与是一遇到false就终止,而不断路与则是全部计算出来再判断。
- 三目运算符
String s = 5>3?"5>3":"5<3"; // 5>3
System.out.println(s);
流程控制
- switch语句,主要说明一点:switch语句后面的控制表达式的数据类型只能是byte,short,char,int4种整数类型,枚举类型,Java.lang.String类型,不能是布尔类型。与之类似的if语句的控制表达式的语句可以是布尔型。还有就是不能省略每个case后面的break语句,否则之后的case不再判断,直接运行下一个case的语句。因为switch的意思实际上是转换的意思,不是判断对错,转换后的选择都是罗列出来了的。如果控制表达式是一个布尔类型,那么将无从匹配case,会引发错误。因此,两种语法其实是有不同的应用场景的。
- break语句,跳出循环。还可以通过标签指定循环,标识符加冒号的形式定义标识符
outer: // outer实际上指定了外层循环
for(var i = 0;i<3;i++){
for(var j=0;j<3;j++)
{
System.out.println("i的值为:" + i +", j的值为:" + j);
if(j==1)break outer; // 这里是break的另一个用法,在其后指定循环标签可跳出指定循环,直接跳出外层循环,程序运行结束!
/*
因此运行结果为:
i的值为:0, j的值为:0
i的值为:0, j的值为:1
*/
}
- continue同样可以指定标识符:
outer:
// outer实际上指定了外层循环
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
System.out.println("i的值为:" + i + ", j的值为:" + j);
if (j == 1) continue outer; // 这里是continue的另一个用法,在其后指定循环标签可跳出指定循环的当前循环,与break的label类似
/*
因此运行结果为:
i的值为:0, j的值为:0
i的值为:0, j的值为:1
i的值为:1, j的值为:0
i的值为:1, j的值为:1
i的值为:2, j的值为:0
i的值为:2, j的值为:1
可以看到,j的值不会超过1,因为一旦为1,会退出外层循环的当次循环。这时只会更新i的值。
*/
}
}
- 除了break和continue之外,同样可以使用return来结束循环,它的原理是通过结束整个方法来结束循环。
数组
看待数组时,一定要将其看作两部分,一部分是数组引用变量,在栈内存中,另一部分是数组对象,在堆内存中,通常数组对象无法直接访问,只能通过数组引用变量去访问数组元素
- 基本类型数组的初始化:
/*
引用数据类型实际上就是指针,该类型的变量就是用来存储地址的,引用类型包括类,接口和数组类型。
这里主要说明数组的内层原理。Java中凡是对象,都在堆内存中建立,而局部变量则是在栈内存中建立。
数组对象也建立在堆内存中,引用类型变量建立在栈内存中。引用类型变量之间的赋值实际上就是将对象地址进行赋值。
*/
// 基本类型数组的出初始化
// 在栈内存中建立了一个引用变量,其指向整型数组对象,由于没有初始化,因此它只是一个代号
int[] iArr;
// 对引用类型变量进行初始化,这时在堆内存中建立了数组对象,并且初始化为0
iArr = new int[5];
// 对数组元素逐个赋值
for(int i=0;i<iArr.length;i++)
iArr[i] = i + 10;
// 逐个输出数组元素
for(var i :iArr)
System.out.println(i);
- 引用类型数组的初始化:
class Person{
public int age;
public double height;
public void info(){
System.out.println("年龄: " + age + ", 身高: " + height);
}
}
public void ptr_array(){
Person[] students = new Person[2]; // 如果最前面的类型不是基本类型,那么数组元素肯定是引用类型
var zhang = new Person();
zhang.age = 16;
zhang.height = 170;
var lee = new Person();
lee.age =16;
lee.height = 161;
students[0] = zhang;
students[1] = lee;
for(var i :students)
i.info();
zhang.age = 18; // students[0]的age也会修改,因为二者指向同一个对象。
lee.height = 180;
for(var i :students)
i.info();
}
- 多维数组
严格来说Java没有多维数组,有的只是引用的引用,不断迭代,因而好像构成了多维数组。在实际使用中要注意,抛开多维数组的概念,始终要记得:所谓的多维,不过是一维一维数组的一个堆叠。
// 定义一个二维数组引用变量
int[][] a;
// 对这个二维数组进行初始化,可以将其当作一维数组来初始化,这又印证了没有多维数组,只有一维数组
a = new int[3][]; // 相当于第一个维度有3个元素,每个元素又是一个数组引用
// 下面是进行第二个维度的初始化,每个数组引用指向的元素个数不相同也是可以的
a[0] = new int[3];
a[1] = new int[5];
a[2] = new int[6];
System.out.println(a[0].length); // 3
System.out.println(a[1].length); // 5
System.out.println(a[2].length); // 6
//也可以像下面这样在初始化的时候就指定好维度,这样可以保证列数是相等的。
int[][] b = new int[3][5];
//对于字符串类型的二维要尤其注意:字符串与字符串数组的区别
String[][] s = new String[3][];
s[0] = new String[4];
- 常见Arrays的方法,用于操作数组的方法:
var a = new int[] {3,4,5,6};
var a2 = new int[] {3,4,5,6};
System.out.println("a和a2的长度和元素是否均相等:" + Arrays.equals(a, a2));
var b = Arrays.copyOf(a, 6);
System.out.println("a和b的长度和元素是否均相等:" + Arrays.equals(a, b));
System.out.println("b数组的元素为:" + Arrays.toString(b));
Arrays.fill(b, 3, 5, 1);
System.out.println("b数组的元素为:" + Arrays.toString(b)); // 输出数组的方法
Arrays.sort(b);
System.out.println("b数组的元素为:" + Arrays.toString(b));
/*
a和a2的长度和元素是否均相等:true
a和b的长度和元素是否均相等:false
b数组的元素为:[3, 4, 5, 6, 0, 0]
b数组的元素为:[3, 4, 5, 1, 1, 0]
b数组的元素为:[0, 1, 1, 3, 4, 5]
*/