Notes 20180307 : 运算符
我们前边曾说过程序=数据结构+算法,数据结构讲的是数据在内存中的存储形式,这个我会作为2018的一个重点来研究,不过在这里不做赘述,前半年的工作以JavaSE为主。算法则是我们在数据结构的基础上对其的一些运算,这些运算可以是逻辑运算也可以是数学运算,这也是本节我想讨论的问题,下面开始本节学习。
1 运算符
运算符是一些特殊的符号,主要用于数学函数、一些类型的赋值语句和逻辑比较方面。Java中提供了丰富的运算符,如赋值运算符、算术运算符、比较运算符等,下面我们来详细了解一下;
前言:运算符与表达式
在开始今天的正式学习之前,先来了解两个概念。
表达式:
表达式是指用运算符连接起来的,符合java语法的运算语句。Java表达式可以分为如下几种:
- 算数表达式:20+10
- 关系表达式:800>200
- 逻辑表达式:10>5 && 100<80
- 赋值表达式:a=10
运算符
单目运算符是指:运算对象只有一个的运算符;如:取正(+)、取负(-)、取反(^)、或(|)、与(&)等等;
双目运算符是运算对象有两个的;如:加(+)减(-)乘(*)除(/)、自加(++)、自减(--)、逻辑与(||)、逻辑或(&&)、取余(%)、赋值(=)等;
三目运算符在Java语言中我知道的就一个(?:)
1.1.1 赋值运算符
赋值运算符“=”,是一个二元运算符(即对两个操作数进行处理),其功能是将右方操作数所含的值赋值给左方的操作数。语法格式如下:
变量类型 变量名 = 所赋的值;
左方必须是一个变量,而右边所赋的值可以是任何数值或表达式,包括变量(如a、number)、常量(如123、“book”)或有效的表达式(如45*12)。赋值运算符“=”处理时会先取得右方表达式处理后的结果,因此一个表达式中若含有两个以上的“=”运算符,会从最右方的“=”开始处理。
注意:因为java中的各种数据说白了都是在内存中的一块区域,所以赋值运算符说白了只是传递了一个指针指向,让变量名指向内存中的变量区域。
【例4.17】创建类EvaluationPractice,在类中进行一些赋值操作
class EvaluationPractice{
public static void main(String[] args){
int ax = 1 , bx = 9 ; //为1个int型变量名为ax的变量赋值,初始化值为1
int cx = ax = bx - ax ; //当一个表达式中若有两个以上的赋值运算符,那么会先从最右边算起,当然我们在程序代码中并不建议这么做.
System.out.println("cx = "+cx);
System.out.print("ax = "+ax);
}
}
赋值运算符除了“=”以外,还有其他几个(+=, -=, *=, /=, %=),我们把它们放在算术运算符中介绍。
1.1.2 算术运算符
如果对负数取模,可以把模数负号忽略不记,如:5%-2=1。但被模数是负数就另当别论(被模数为负数要考虑符号)。对于除号“/”,它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃小数部分。 例如:int x=4150;x=x/1000*1000; x的结果是?“+”除字符串相加功能外,还能把非字符串转换成字符串 ,例如:System.out.println("5+5="+5+5);//打印结果是?
【例4.18】创建ArithmeticPractice类,进行一些操作
class ArithmeticPractice{
public static void main(String[] args){
int ax = 3 , bx = 2 , hx , jx = -1 , rx = -5;
byte cx = (byte)(ax + bx) ;
long dx = bx - ax ;
float ex = ax%bx ;
System.out.println("ax除以bx取余 = "+ex);
ex = ax%jx ; //取余可以快速用来判断一个数的奇偶性
System.out.println("当被模数是负数时:"+ex);
ex = rx%bx ;
System.out.println("当模数是负数时:"+ex);//当模数是负数时,要考虑符号
ex = rx%-3 ;
System.out.println("当模数和被模数都为负数时会先将模数的符号撇去然后运
算,实际在取模运算中只考虑被模数的取值符号,正则正,负则负:"+ex);
int fx = ax * bx ;
hx = 0 ;
//int gx = ax/hx ; 除数不能为0,否则会报错误,java.lang.ArithmeticException
int gx = ax / bx ; //除的结果为整数,余数会舍掉
System.out.println("ax+bx = "+cx);
System.out.println("bx-ax = "+dx);
System.out.println("ax*bx = "+fx);
System.out.println("ax/bx = "+gx);
System.out.println("5+5 ="+5+5);//+除了可以做加法外,还可以把非字符串转换成字符串
}
}
赋值运算符中除了“=”还有“+=”,“-=”,“ *=”,“ /=”,“ %=”,这些运算符会先完成赋值运算符前面的运算然后再赋值,这些运算符具有默认强制类型装换功能(这个要理解,有助于一些面试题的正确回答),我们做几个事例,帮助理解,int a,b,c; a=b=c =3; int a = 3; a+=5;等同运算a=a+5;
思考:short s = 5; s=s+2; s+=2; 有什么区别?
【例4.18】我们在EvaluationPractice类中,作如下测试
//short s = 5 ;
//s = s + 2 ; //赋值运算符左边的变量s是short型,右边的在做运算时会先转换成默认int型,运算结果也是int型,而把右边的int型转换为short型,这是由高精度向低精度转换,会损失精度
//s+=2 ; //s+=2 在short类型下,它会强制转换成short类型
//System.out.println(s); //运算结果为7
byte s = 5 ;
s+=123;
/*8位byte类型二进制的范围为-128到127,因为首位为符号位,所以正数到127-01111111,就到头了,所以正数最大为127;-128的二进制位128的补码10000000,因为“正数128的二进制为10000000”,这是关键点,有人会想“正数128的二进制为10000000”这不负数吗,其实不然,只是byte类型首位为符号位而引起的,否则正数最大也不会是127,*/
System.out.print("损失精度:"+s);
上面是针对整型的运算,那么小数呢?
//浮点型的除法与取模
public static void test(){
float f = 1.23F;
int i = 2;
System.out.println("浮点除法:"+f/i);
System.out.println(f/0.25);
System.out.println(f%1);
}
/** * 测试整数类型和浮点数之间的数学运算 */ @Test public void test(){ int i = 23; float f = 1.2F; System.out.println(i/f); float f1 = i/23; System.out.println(f1); }
在这里我们发现有些结果是出乎我们意料的,在这里就牵涉到了类型转换,整数在做数学运算时,其结果也是整数类型的,但是如果牵涉到了类型不同,那么会先将类型转变为高精度类型,然后运算,运算结果再与结果类型(即赋值运算符左侧变量)比对,如果类型相同则相同,不同则进行类型转换。在上面中如果数学运算中有浮点型,那么就按照浮点型进行运算,结果就是我们观察到的具有小数点的,如果是正数,那么就按照整数运算(不存在四舍五入的情况),这个我们要了解,实际情况中关于数学运算我们大多选择BigDecember。
System.out.println(23/22);//1 System.out.println(22/23);//0
1.1.3 自增和自减运算符
自增、自减运算符是单目运算符,可以放在操作元之前,也可以放在操作元之后。操作元必须是一个整型或浮点型变量。放在操作元前面的自增、自减运算符,会先将变量的值加1(减1),然后再使该变量参与表达式的运算;放在操作元后面的自增、自减运算符,会先使变量参与表达式的运算,然后再将该变量加1(减1)。示例代码如下:
++a(--a) //表示在使用变量a之前,先使a的值加(减)1
a++(a--) //表示在使用变量a之后,使a的值加(减)1
【例4.19】我们在Demo类中,进行下面一些验证
class Demo{
public static void main(String[] args){
int ax = 3 , bx = 2 , jx = -1;
ax = bx++ + jx ;
System.out.println("前加加和后加加的区别:"+ax+"......"+bx+"......"+ax);
bx = ++ax + jx ;
System.out.println("前加加和后加加的区别:"+ax+"......"+bx+"......"+ax);
}
}
在我们第一次运算时bx先进行和jx的运算,然后赋值给ax,最后进行了一次自加加,这时的输出结果如同第一次输出时一样,然后在做第二次运算时,ax这时先进行一次自加加,然后再进行和jx的运算,最后结果赋值给bx。注意如果我们在输出语句中进行++输出,那么会仍然遵循自运算的特性,如下:
int a = 23; System.out.println(a++ + "---" + ++a);//23---25
1.1.4 比较运算符
比较运算符里要注意==和=我们要注意区分开,一个是等于,一个是赋值是不一样的
【例4.20】创建ComparisonOperatorPractice类,进行一些操作
class ComparisonOperatorPractice{
public static void main(String[] args){
int s1 = 5 , s2 = 6 ;
System.out.println("5>6这是真的吗:"+(s1>s2));
System.out.println("5<6这是真的吗:"+(s1<s2));
System.out.println("5等于6是真的吗:"+(s1==s2));
System.out.println("5大于等于6是真的吗:"+(s1>=s2));
System.out.println("5小于等于6是真的吗:"+(s1<=s2));
System.out.println("5是不等于6的:"+(s1!=s2));
System.out.println("字符S和s是不相同的:"+('S'!='s'));
}
}
比较运算符中,“==”和“!=”除了可以用于基本数据类型外,还可以用于比较引用数据类型,实际上这两个运算符都是通过比较内存地址,来判断结果的。我们在字符串中见到的很多要注意一下,如下代码
String str1 = "s"; String str2 = "s"; String str3 = new String("s"); System.out.println(str1 == str2);//true System.out.println(str1 == str3);//false
在这里对于引用类型的比较,就介绍到这里,知道“!=”和“==”可以用来比较引用数据类型就可以了(通过比对引用类型内存地址),关于“==”我们会在讲解Object时和equals()比较讲解;
1.1.5 逻辑运算符
返回类型为布尔值的表达式,如比较运算符,可以被组合在一起构成一个更复杂的表达式。这是通过逻辑运算符来实现的。逻辑运算符包括&&(&)(逻辑与)、||(|)(逻辑或)、!(逻辑非)和^(逻辑异或),返回值为布尔类型的表达式,操作元也必须是boolean类型数据。与比较运算符相比,逻辑运算符可以表达更加复杂的条件,如连接几个关系表达式进行判断(比如我们数学中有这么个表达式在Java中不可以写成3<x<6,应该写成x>3 & x<6)。在逻辑运算符中,除了“!”是一元运算符外,其余的都是二元运算符,用法如下:
“&”和“&&”的区别:
单&时,左边无论真假,右边都进行运算;它属于非短路运算符
双&时,如果左边为真,右边参与运算,如果左边为假,那么右边不参与运算。他属于短路运算符
“|”和“||”的区别同理,双或时,左边为真,右边不参与运算。
异或( ^ )与或( | )的不同之处是:当左右都为true时,结果为false。
【例4.21】创建LogicalOperatorPractice类,进行上述一些验证
package cn.basic.operator;
class LogicalOperatorPractice{
public static void main(String[] args){
boolean b1 = 3 > 5 , b2 = 3 < 5 ;
System.out.println("逻辑单与,真真为真,真假为假,假真为假,假假为假:"+(b2&b2)+".."+(b2&b1)+".."+(b1&b2)+".."+(b1&b1));//单&时,左边无论真假,右边都进行运算;
System.out.println("逻辑双与,真真为真,真假为假,假真为假,假假为假:"+(b2&&b2)+".."+(b2&&b1)+".."+(b1&&b2)+".."+(b1&&b1));//双&时,如果左边为真,右边参与运算,如果左边为假,那么右边不参与运算。
System.out.println("逻辑单或,真真为真,真假为真,假真为真,假假为假:"+(b2|b2)+".."+(b2|b1)+".."+(b1|b2)+".."+(b1|b1));//单|时,左边无论真假,右边都进行运算;
System.out.println("逻辑双或,真真为真,真假为真,假真为真,假假为假:"+(b2||b2)+".."+(b2||b1)+".."+(b1||b2)+".."+(b1||b1));//双||时,如果左边为真,右边不参与运算,如果左边为假,那么右边参与运算。
System.out.println("逻辑异或,真真为假,真假为真,假真为真,假假为假:"+(b2^b2)+".."+(b2^b1)+".."+(b1^b2)+".."+(b1^b1));
System.out.println("逻辑非,真为假,假为真,"+!b2+".."+!b1);
}
}
1.1.6 位运算符
位运算符除按位与和按位或运算符外,其它只能用于处理整数的操作数。位运算是完全针对位方面的操作。整型数据在内存中是以二进制的形式表示,如int型变量7的二进制表示是00000000 00000000 00000000 00000111。左边最高位是符号位,最高数是0表示正数,若为1则表示负数。数据在计算机中采用补码表示,如-8的二进制表示为11111111 11111111 11111111 11111000.这样就可以对整数数据进行按位运算。Java语言提供的位运算符主要有下面几种。
下面我们来挨个认识一下位运算符;
1.1.6.1 “按位与”运算
“按位与”运算的运算符为“&”,是双目运算符。其运算的法则是:如果两个操作数对应位都是1,则结果才是1,否则为0。如果两个操作数的精度不同,则结果的精度与精度高的操作数相同。
判断一个数n的奇偶性
n&1 == 1?”奇数”:”偶数”
为什么与1能判断奇偶?所谓的二进制就是满2进1,那么好了,偶数的最低位肯定是0(恰好满2,对不对?),同理,奇数的最低位肯定是1,(二进制与十进制的转换决定了二进制最后一位为1*2^0或者0*2^0).int类型的1,前31位都是0,无论是1&0还是0&0结果都是0,那么有区别的就是1的最低位上的1了,若n的二进制最低位是1(奇数)与上1,结果为1,反则结果为0.
@Test
public void test(){
fun1(8);
fun1(7);
}
/**
* 按位与的运用
* 判断奇数和偶数
*/
public void fun1(int i){
if((i&1)==1){
System.out.println("该数为奇数:"+i);
}else {
System.out.println("该数为偶数:"+i);
}
}
0000 0000 0000 0000 0000 0000 0000 1000 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0000 0000 0000 0000 0000 |
0000 0000 0000 0000 0000 0000 0000 0111 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0000 0000 0000 0000 0001 |
上面我们知道了按位与可以用来判断奇偶,那么n取2的模数呢?是不是同样可以呢?如果是奇数,那么n%2==1,如果是偶数那么n%2==0.
1.1.6.2 “按位或”运算
1.1.6.3 “按位取反”运算
取相反数
给出任意一个数求它的相反数,怎么样最快捷;读这道题,如果只读前半句,那很简单,加个-号不就行了吗,可讲到最快捷,那么是什么呢?会是按位取反吗?
/**
* 按位取反
*/
public void fun2(int i,int in){
long startTime1 = System.currentTimeMillis();
for (int j = 0; j < 10000000; j++) {
i = -(i+j);
// System.out.println(i);
}
long endTime1 = System.currentTimeMillis();
System.out.println("用负号取反用时:"+(endTime1-startTime1));
startTime1 = System.currentTimeMillis();
for (int j = 0; j < 10000000; j++) {
i = ~(i+j)+1;
// System.out.println(i);
}
endTime1 = System.currentTimeMillis();
System.out.println("用按位取反用时:"+(endTime1-startTime1));
System.out.println("in = "+in);
System.out.println("取反值:"+(-in));
System.out.println("取反值:"+(~in+1));
}
fun2(9,23);
多次运行我们发现按位取反都会比用负号慢,这是为什么呢?虽然按位取反直接作用在二进制位上,但是按位取反还涉及到了一个加法,所以比较起负号就慢了;
23的二进制是:00000000 000000000 00000000 00010111 按位取反 11111111 11111111 11111111 11101000 负数补码 转变为原码是除符号位取反末尾加1 10000000 00000000 00000000 00011000 =-24 +1 -23 |
1.1.6.4 “按位异或”运算
数组首尾交换
我们学过数组会遇到这么一个问题:”将数组的索引1元素和最后一个索引元素互换,索引2元素和倒数第二个元素互换,要求不能声明第三个变量;”初次看到这个问题我们都会愕然,这怎么做啊,a=arr[0],b=arr[arr.length-1],那么接下来呢?a=b,那么arr[arr.length-1]就丢失了,那么该怎么解决呢?按位异或可以帮助我们解决这个问题;
/**
* 数组头尾交换
*/
public void fun3(){
int[] strArr = {1,2,3,4,5,6,7,8,9};
int i = 0;
int j = strArr.length-1;
while (j>i) {
strArr[i] = strArr[i]^strArr[j];
strArr[j] = strArr[i]^strArr[j];
strArr[i] = strArr[i]^strArr[j];
System.out.println(strArr[i]+"---"+strArr[j]);
i++;
j--;
}
}
那么这是什么原理呢?为什么就莫名其妙实现了呢?在分析之前,我们先多说一句按位运算是作用在二进制位上的,所以操作元应该是整数类型;
分析:举例arr[0] = 111;arr[n] = 222;换算二进制;
0000 0000 0000 0000 0000 0000 0110 1111 arr[0] 111
^
0000 0000 0000 0000 0000 0000 1101 1110 arr[n] 222
arr[0] = arr[0] ^ arr[n];
0000 0000 0000 0000 0000 0000 1011 0001 arr[0] 177
^
0000 0000 0000 0000 0000 0000 1101 1110 arr[n] 222
arr[n ] = arr[0]^arr[n];
0000 0000 0000 0000 0000 0000 0110 1111 arr[n] 111
^
0000 0000 0000 0000 0000 0000 1011 0001 arr[0] 177
arr[0]=arr[0]^arr[n];
0000 0000 0000 0000 0000 0000 1101 1110 arr[0] 222
经过上面我们的分析,我们知道其实他们只是在不停的按位异或以达到将数不停的交换;
1.1.6.5 移位操作
【例4.22】创建ShiftOperatorPractice类,进行移位运算符的验证操作
class ShiftOperatorsPractice{
public static void main(String[] args){
System.out.println("46&97的结果为:"+(46&97)); //46的二进制表示00000000 00000000 00000000 00101110
System.out.println("46|97的结果为:"+(46|97)); //97的二进制表示00000000 00000000 00000000 01100001
System.out.println("46按位取反的结果为:"+~46);
System.out.println("46^97的结果为:"+(46^97));
System.out.println("46左移5位的结果是:"+(46<<5));
System.out.println("46右移2位的结果是:"+(46>>2));
System.out.println("46无符号右移1位的结果是:"+(46>>>1));
}
}
思考题: 最有效率的方式算出2乘以8等于几?
对两个整数变量的值进行互换(不需要第三方变量)
【例4.23】创建类ShiftOperatorDemo,进行如下操作;
class ShiftOperatorDemo{
public static void main(String[] args){
int A = 23 , B = 46 ; //46的二进制:00000000 00000000 00000000 00101110
A = A ^ B ; //23的二进制:00000000 00000000 00000000 00010111
B = A ^ B ; //A^B 00000000 00000000 00000000 00111001
A = A ^ B ;
System.out.println("A="+A+","+"B="+B);
System.out.println("有效率的方式算出2乘以8的方法为2<<3:"+(2<<3));
}
}
1.1.6.6 位移运算符练习
[例1]计算一个数的2的n次方;除以一个数的n次方
fun4(2, 4);
/**
* 计算n次方
*/
public void fun4(int i,int n){
System.out.println(i+"的"+n+"次方是:"+(i<<n));
System.out.println(32>>3);
}
[例2]求一个数的绝对值
(a^(a>>31))-(a>>31)
先整理一下使用位运算取绝对值的思路:若a为正数,则不变,需要用异或0保持的特点;若a为负数,则其补码为源码翻转每一位后+1,先求其源码,补码-1后再翻转每一位,此时需要使用异或1具有翻转的特点。
任何正数右移31后只剩符号位0,最终结果为0,任何负数右移31后也只剩符号位1,溢出的31位截断,空出的31位补符号位1,最终结果为-1.右移31操作可以取得任何整数的符号位。
那么综合上面的步骤,可得到公式。a>>31取得a的符号,若a为正数,a>>31等于0,a^0=a,不变;若a为负数,a>>31等于-1 ,a^-1翻转每一位.
小结
在日常的java开发中位运算使用的不是很常见,但是面试或考试中会有涉及的地方,虽然不是决定项,但却是加分项,说明对计算机语言有最起码的了解。而且在高级算法中,位运算往往能优化算法运行效率,减少运行时间。再比如,有一张全是选择题或是勾选题(类似判断)的试卷,你是使用每个选项一条记录的形式保存答案还是使用一个二进制对应的整数来保存答案?就像是英语考试中的答题卡:
每个题目有4个选项,每个选项有两个状态:选、不选(1、0),那么此时是不是可以使用4位二进制数来表示某题的答案呢?
fun5(-6);
fun5(7);
/**
* 求一个数的绝对值
*/
public void fun5(int a){
System.out.println("绝对值:"+((a^(a>>31))-(a>>31)));
}
[例3]求平均值,比如有两个int类型变量x、y,首先要求x+y的和,再除以2,但是有可能x+y的结果会超过int的最大表示范围,所以位运算就派上用场啦。(x&y)+((x^y)>>1);
fun6(999456789, 1234567898);
/**
* 求两个数的平均值
*/
public void fun6(int x,int y){
System.out.println("平均值:"+((x&y)+(x^y)>>1));
}
[例4] 对于一个大于0的整数,判断它是不是2的几次方 ((x&(x-1))==0)&&(x!=0);
/**
* 判断一个数是不是2的n次方
*/
public void fun7(){
int x = 98768676;
System.out.println(((x&(x-1))==0)&&(x!=0));
x = 32;
System.out.println(((x&(x-1))==0)&&(x!=0));
}
1.1.7 三元运算符
三元运算符是Java中唯一一个三目运算符,其操作元有3个,第一个是条件表达式,其余的是两个值,条件表达式成立时运算取第一个值,不成立时取第二个值。
boolean b = 20 < 45 ? true : false;
三元运算符用于判断,等价的if...else语句如下:
boolean a; //声明boolean变量
If(20 < 45) a = true;
else a = false;
当表达式20<45的运算结果返回真时,则boolean型变量a取值true;当表达式20《45返回假时,则boolean型变量a取值false,此例的结果是true。
【例4.23】创建TernaryOperatorPractice类,并进行一些运算,获取两个数中的最大数
class TernaryOperatorPractice{
public static void main(String[] args){
int i = 23 , j = 45 ;
int z = i > j ? i : j ;
System.out.println(z);
}
}
1.1.8 运算符优先级
【例4.24】创建PriorityPractice类,验证一些运算符的优先级;
class PriorityPractice{
public static void main(String[] args){
int a = 20 ;
a = a++ + 1 ;
System.out.println("我们一定要注意运算符的优先级及其在计算机内存中的运算"+a);
a = ++a + 1 ;
System.out.println("我们一定要注意运算符的优先级及其在计算机内存中的运算"+a);
}
}
分析:这个过程分开分析是这样的a=a++----> b=a; a=a+1; a=b;
a=a++ + 1在计算机的执行过程是,1. 备份a+1的值,2. 把a的值加1, 3. 把备份的值再赋值给a
那 a = ++a + 1,就是 1.把a的值加1,2.备份a+1+1的值,3,把a+1+1的值赋值给a