如何操作数据?
续上期《数据是什么?何为数据?》本文和大家一起学习如何操作数据
对数据进行的操作可以有:
- 赋值;
- 基本运算。
一、赋值
在上节我们说了数据类型和变量,通过声明变量,每个变量赋予一个数据类型和一个有意义的名字,我们就告诉了计算机我们要操作的数据。
有了数据,我们能做很多操作。但本文只说说对数据做的第一个操作:赋值。声明变量之后,就在内存分配了一块位置,但这个位置的内容是未知的,赋值就是把这块位置的内容设为一个确定的值。
Java中基本类型、数组、对象的赋值有明显不同,本文介绍基本类型和数组的赋值,关于对象后续文章会详述。
基本类型的赋值:
整数类型:
整数类型有byte
, short
, int
和long
,分别占用1/2/4/8个字节,取值范围分别是:
(在《数据是什么?何为数据?》中有详细介绍了数据类型和取值范围)
整型的赋值形式很简单,直接把熟悉的数字常量形式赋值给变量即可,对应的内存空间的值就从未知变成了确定的常量。但常量不能超过对应类型的表示范围。例如:
byte b = 23;
short s = 3333;
int i = 9999;
long l = 32323;
特别地:
在给
long
类型赋值时,如果常量超过了int
的表示范围,需要在常量后面加大写或小写的L,即L或l,例如:(这个是由于数字常量默认为是int类型。)long a = 3232343433L;
小数类型
小数类型有float和double,占用的内存空间分别是4和8个字节,有不同的取值范围和精度,double表示的范围更大,精度更高,具体来说:
小数类型与取值范围图取值范围看上去很奇怪,一般我们也不需要记住,有个大概印象就可以了。E表示以10为底的指数,E后面的+号和-号代表正指数和负指数,例如:1.4E-45表示1.4乘以10的-45次方。后续文章会进一步分析小数的二进制表示以及表示范围为什么会是这样的。
对于double,直接把熟悉的小数表示赋值给变量即可,例如:
double d = 333.33;
但对于float,需要在数字后面加大写F或小写f,例如:(这个是由于小数常量默认为是double类型。)
float f = 333.33f;
除了小数,也可以把整数直接赋值给float或double,例如:
float f = 33;
double d = 3333333333333L;
boolean类型
这个很简单,直接使用true或false赋值,分别表示真和假,例如:
boolean b = true;
b = false;
字符类型
字符类型char用于表示一个字符,这个字符可以是中文字符,也可以是英文字符。在内存中,Java用两个字节表示一个字符。赋值时把常量字符用单引号括起来,不要使用双引号,例如:
char c = 'A';
char z = '中';
特别地:
上面介绍的赋值都是直接给变量设置一个常量值。但也可以把变量赋给变量,例如:int a = 100; int b = a;
变量可以进行各种运算(后续文章讲解),也可以将变量的运算结果赋给变量,例如:
int a = 1;
int b = 2;
int c = 2*a+b; //2乘以a的值再加上b的值赋给c
上面介绍的赋值都是在声明变量的时候就进行了赋值,但这不是必须的,可以先声明变量,随后再进行赋值。
数组类型
赋值语法
基本类型的数组有三种赋值形式,如下所示:
1. int[] arr = {1,2,3};
2. int[] arr = new int[]{1,2,3};
3. int[] arr = new int[3];
arr[0]=1; arr[1]=2; arr[2]=3;
第一种和第二种都是预先知道数组的内容,而第三种是先分配长度,然后再给每个元素赋值。
第三种形式中,即使没有给每个元素赋值,每个元素也都有一个默认值,这个默认值跟数组类型有关。数值类型的值为0,boolean为false, char为空字符。
数组长度可以动态确定,如下所示:
int length = ... ;//根据一些条件动态计算
int arr = new int[length];
虽然可以动态确定,但定了之后就不可以变,数组有一个length属性,但只能读,不能改。
一个小细节,不能在给定初始值的同时还给定长度,即如下格式是不允许的:
int[] arr = new int[3]{1,2,3}
这是可以理解的,因为初始值已经决定了长度,再给个长度,如果还不一致,计算机将无所适从。
数组和基本类型的区别
一个基本类型变量,内存中只会有一块对应的内存空间。但数组有两块,一块用于存储数组内容本身,另一块用于存储内容的位置。
用一个例子来说明,有一个int变量a,和一个int数组变量arr,其代码,变量对应的内存地址和内存内容如下所示:
image.png基本类型a的内存地址是1000,这个位置存储的就是它的值100。
数组类型arr的内存地址是2000,这个位置存储的值是一个位置3000,3000开始的位置存储的才是实际的数据1,2,3。
为什么数组要用两块空间
不能只用一块空间吗?我们来看下面这个代码:
int[] arrA = {1,2,3};
int[] arrB = {4,5,6,7};
arrA = arrB;
这个代码中,arrA初始的长度是3,arrB的长度是4,后来将arrB的值赋给了arrA。如果arrA对应的内存空间是直接存储的数组内容,那么它将没有足够的空间去容纳arrB的所有元素。
用两块空间存储,这个就简单的多,arrA存储的值就变成了和arrB的一样,存储的都是数组内容{4,5,6,7}的地址,此后访问arrA就和arrB是一样的了,而arrA {1,2,3}的内存空间由于无人引用会被垃圾回收,如下图所示:
arrA {1,2,3}
\
\
arrB -> {4,5,6,7}
由上,也可以看出,给数组变量赋值和给数组中元素赋值是两回事。给数组中元素赋值是改变数组内容,而给数组变量赋值则会让变量指向一个不同的位置。
上面我们说数组的长度是不可以变的,不可变指的是数组的内容空间,一经分配,长度就不能再变了,但是可以改变数组变量的值,让它指向一个长度不同的空间,就像上例中arrA后来指向了arrB一样。
归纳:
给变量赋值就是将变量对应的内存空间设置为一个明确的值,有了值之后,变量可以被加载到CPU,CPU可以对这些值进行各种运算,运算后的结果又可以被赋值给变量,保存到内存中。
二、基本运算
运算
有了初始值之后,可以对数据进行运算。计算机之所以称为"计算"机,是因为发明它的主要目的就是运算。运算有不同的类型,不同的数据类型支持的运算也不一样,本文介绍Java中基本类型数据的主要运算。
- 算术运算:主要是日常的加减乘除
- 比较运算:主要是日常的大小比较
- 逻辑运算:针对布尔值进行运算
算术运算
算术运算符有加减乘除,符号分别是+-*/,另外还有取模运算符%,以及自增(++)和自减(–)运算符。取模运算适用于整数和字符类型,其他算术运算适用于所有数值类型和字符类型,其他都符合常识,但字符类型看上去比较奇怪,后续文章解释。
减号(-)通常用于两个数相减, 但也可以放在一个数前面,例如 -a, 这表示改变a的符号,原来的正数会变为负数,原来的负数会变为正数,这也是符合我们常识的。
取模(%)就是数学中的求余数,例如,5%3是2,10%5是0。
自增(++)和自减(--),是一种快捷方式,是对自己进行加一或减一操作。
加减乘除大部分情况和直观感觉是一样的,都很容易理解,但有一些需要注意的地方,而自增自减稍微复杂一些,下面我们解释下。
加减乘除注意事项
运算时要注意结果的范围,使用恰当的数据类型。两个正数都可以用int表示,但相乘的结果可能就会超,超出后结果会令人困惑,例如:int a = 2147483647*2; //2147483647是int能表示的最大值
a的结果是-2。为什么是-2我们暂不解释,要避免这种情况,我们的结果类型应使用long,但只改为long也是不够的,因为运算还是默认按照int类型进行,需要将至少一个数据表示为long形式,即在后面加L或l,下面这样才会出现期望的结果:
long a = 2147483647*2L;
另外,需要注意的是,整数相除不是四舍五入,而是直接舍去小数位,例如:
double d = 10/4;
结果是2而不是2.5,如果要按小数进行运算,需要将至少一个数表示为小数形式,或者使用强制类型转化,即在数字前面加(double),表示将数字看做double类型,如下所示任意一种形式都可以:
double d = 10/4.0; double d = 10/(double)4;
以上一些注意事项,我想也没什么特别的理由,大概是方便语言设计者实现语言吧。
小数计算结果不精确
无论是使用float还是double,进行运算时都会出现一些非常令人困惑的现象,比如:float f = 0.1f*0.1f; System.out.println(f);
这个结果看上去,不言而喻,应该是0.01,但实际上,屏幕输出却是0.010000001,后面多了个1。换用double看看:
double d = 0.1*0.1; System.out.println(d);
屏幕输出0.010000000000000002,一连串的0之后多了个2,结果也不精确。
这是怎么回事?看上去这么简单的运算,计算机怎么能计算不精确呢?但事实就是这样,究其原因,我们需要理解float和double的二进制表示,后续文章进行分析。
自增(++)/自减(--)
自增/自减是对自己做加一和减一操作,但每个都有两种形式,一种是放在变量后,例如a++, a--,另一种是放在变量前,例如++a, --a。如果只是对自己操作,这两种形式也没什么差别,区别在于还有其他操作的时候。放在变量后(a++),是先用原来的值进行其他操作,然后再对自己做修改,而放在变量前(++a),是先对自己做修改,再用修改后的值进行其他操作。例如,快捷运算和其等同的运算分别是:
image.png自增/自减是"快捷"操作,是让程序员少写代码的,但遗憾的是,由于比较奇怪的语法和诡异的行为,带给了初学者一些困惑。
比较运算
比较运算就是计算两个值之间的关系,结果是一个布尔类型(boolean)的值。比较运算适用于所有数值类型和字符类型。数值类型容易理解,但字符怎么比呢?后续文章解释。比较操作符有:大于(>),大于等于(>=),小于(<),小于等于(<=),等于(==),不等于(!=)。
大部分也都是比较直观的,需要注意的是等于。
首先,它使用两个等号==,而不是一个等号(=),为什么不用一个等号呢?因为一个等号(=)已经被占了,表示赋值操作。
另外,对于数组,==判断的是两个数组是不是同一个数组,而不是两个数组的元素内容是否一样,即使两个数组的内容是一样的,但如果是两个不同的数组,==依然会返回false,如下所示:
int[] a = new int[] {1,2,3}; int[] b = new int[] {1,2,3}; // a==b的结果是false
如果需要比较数组的内容是否一样,需要逐个比较里面存储的每个元素。
逻辑运算
逻辑运算根据数据的逻辑关系,生成一个布尔值true或者false。逻辑运算只可应用于boolean类型的数据,但比较运算的结果是布尔值,所以其他类型数据的比较结果可进行逻辑运算。逻辑运算符具体有:
- 与(&):两个都为true才是true,只要有一个是false就是false
- 或(|):只要有一个为true就是true,都是false才是false
- 非(!):针对一个变量,true会变成false, false会变成true
- 异或(^):两个相同为false, 两个不相同为true
- 短路与(&&): 和&类似,不同之处马上解释
- 短路或 (||):与|类似,不同之处马上解释
逻辑运算的大部分都是比较直观的,需要注意的是&和&&,以及|和||的区别。如果只是进行逻辑运算,它们也都是相同的,区别在于同时有其他操作的情况下,例如:boolean a = true; int b = 0; boolean flag = a | b++>0;
因为a为true,所以flag也为true,但b的结果为1,因为|后面的式子也会进行运算,即使只看a已经知道flag的结果,还是会进行后面的运算。而||则不同,如果最后一句的代码是:
boolean flag = a || b++>0;
则b的值还是0,因为||会"短路",即在看到||前面部分就可以判定结果的情况下,忽略||后面的运算。
这个例子我们还可以看出,自增/自减操作带给我们的困扰,别的操作都干干脆,赋值就赋值,加法就加法,比较就比较,它非混在一起,可能会少写些代码,但如果使用不当,会使理解困难很多。
运算符优先级
一个稍微复杂的运算可能会涉及多个变量,和多种运算,那哪个先算,哪个后算呢?程序语言规定了不同运算符的优先级,有的会先算,有的会后算,大部分情况下,这个优先级与我们的常识理解是相符的。但在一些复杂情况下,我们可能会搞不明白其运算顺序。但这个我们不用太操心,可以使用括号()来表达我们想要的顺序,括号里的会先进行运算,简单的说,不确定顺序的时候,就使用括号。
归纳:
上面介绍了算术运算,比较运算和逻辑运算,但我们遗留了一些问题,比如:
- 正整数相乘的结果居然出现了负数
- 非常基本的小数运算结果居然不精确
- 字符类型怎么也可以进行算术运算和比较
这是怎么回事呢?在后续文章中会为小伙伴们解除疑惑。
继续更新,敬请期待!下一期和大家学习如何用二进制表示整数与位运算