java基础(二) 自增自减与贪心规则
引言
JDK中提供了自增运算符++,自减运算符--。这两个操作符各有两种使用方式:前缀式(++ a,--a),后缀式(a++,a--)。可能说到这里,说不得有读者就会吐槽说,前后缀式都挺简单的,前缀式不就是先进行+1(或-1),然后再使用该值参与运算嘛,后缀式则相反。有必要长篇大论吗?
前后缀式的区别确实是这样,最起码表面上理解起来是这样,但是更深入的理解就不是这么简单了,甚至严重影响到你的程序的正确性。不信,接下去看吧!
1. 前缀式 与 后缀式的真正区别
在Java中,运算是从左往右计算的,并且按照运算符的优先级,逐一计算出表达式的值,并用这个值参与下一个表达式的运算,如:1+2+3,其实是先计算出1+2
表达式的值为3,再参与下一个表达式的运算(1+2)+3
,即3+3
。再如判断if(a+2==3)
。如此类推。
a++
是一个表达式 ,那么a++
就会有一个表达式的计算结果,这个计算结果就是a的旧值(加1前的值)。相对的,++a
表达式的计算结果a加1后的值。所以,自增的前缀形式与后缀形式的本质区别是:表达式的值(运算结果) 是加1前的变量的值还是加1后的变量的值(自减也是如此)。并不是先加1 与 后加1的区别,或者说,前后缀形式都是先加1(减1)的,才得到表达式的值,再参与下一步运算。因为这是一个表达式,必须先计算完表达式的运算,最后才会得到表达式的值
我们来看一个面试经常遇到的问题:
int a = 5;
a = a++;
//此时a的值是多少?
有猜到a的值吗?我们用上面所学到的分析一下:a=a++
可以理解成以下几个步骤:
1> 计算a自加1,即 a=a+1
2> 计算表达式的值,因为这是后缀形式,所以a++
表达式的值就是加1前a的值(值为5);
3> 将表达式的值赋值给a,即a=5
。
所以最后a的值是5。
同理,如果改成a = ++a;
,则a的值是6。这是因为++a
表达式的值是加1后的a,即为6。
2. 自增自减是包含两个两个操作,不是线程安全的
自增、自减运算符本质上不是一个计算操作,而是两个计算操作。以a++
为例,这个运算将会编译器解析成:a=a+1
,即包含两个单目运算符(+、=),一个单目运算符的计算操作可以看作是一个原子性操作。a++
的步骤可以描述成 :1> 先取a加1,将结果存储在临时空间;2>将结果赋给a。所以,自增自减运算符包含两个操作:一个加1(减1)的操作和一个赋值的操作
这个原理好像对我的编程没有用吧?不是的,在单线程的环境下,你可以不管这个细节。但在多线程的情况下,你就得时刻牢记这个细节了。要知道,自增自减不是原子性操作,也就是说不是线程安全的运算。因此,在多线程下,如果你要对共享变量实现自增自减操作,就要加锁,或者使用JDK提供的原子操作类(如AtomincInteger
,AtomicLong
等)提供的原子性自增自减方。
来看个例子,验证一下。下面的例子提供三个静态变量(一个是原子操作类),创建了10个线程,每个线程都对这三个变量以不同的方式进行加1操作,并循环1000次。
public class MyTest {
static int a = 0;
static int b = 0;
//原子性操作类
static AtomicInteger atomicInt = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {//创建10个线程
Thread t = new Thread() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {//计算1000次
a = a + 1;
b++;
atomicInt.incrementAndGet();//自增的原子性方法
}
}
};
t.start();
}
// 判断当前的活动线程是不是只有main线程,以确保10个计算线程执行完成。
while (Thread.activeCount() > 1) {
Thread.sleep(1000);
}
System.out.println("a=a+1在多线程下的结果是:" + a);
System.out.println("b++在多线程下的结果是:" + b);
System.out.println("原子操作类AtomicInteger在多线程下的结果是:" + atomicInt.get());
}
}
运行结果:
a=a+1在多线程下的结果是:8883
b++在多线程下的结果是:8974
原子操作类AtomicInteger在多线程下的结果是:10000
从运行的结果可以看出,a=a+1
、b++
不是线程安全的,没有计算出正确的结果10000。也就是说这两个表达式都不是原子性操作。事实上,它们都包含了两个计算操作。
3. 由 a+++b 表达式引起的思考
看到这个表达式,真的很让人疑惑:编译器是怎么解析的,是解析成
a++ + b
还是
a+ ++b
真纠结,干脆直接在编译器上跑一趟,看看结果吧!
int a = 5;
int b = 5;
int c=a+++b;
System.out.println("a的值是: "+a);
System.out.println("b的值是: "+b);
运行结果:
a的值是: 6
b的值是: 5
从结果可以确认,a+++b
其实是解析成了 a++ +b
,为什么要这样结合呢?其实有两点原因:
- Java中的运算是从左往右进行的;
- java编译器有一种规则——贪心规则。也就是说,编译器会尽可能多地结合有效的符号。
那么,a+++b
这样的结合方式就可以解释了
但是这种结合是:尽可能多的结合,而不管这样的结合是否合法。如:
a--b
会被编译器解析成
a-- b
尽管这是不合法,但编译器还是这样处理了,这就导致编译不通过,产生编译错误。
编译器为什么要采用贪心规则呢?
从上面的分析来看,贪心规则在编程中也不好利用。那么,贪心规则的主要目的是为了什么?
贪心规则的主要目的就是为了分析String字符串,看看下面的例子就会明白:
String s = "\17";
System.out.println("\\17 转义字符的值是:"+s+" 长度是:"+s.length());
s = "\171";
System.out.println("\\171 转义字符的值是:"+s+" 长度是:"+s.length());
s = "\1717";
System.out.println("\\1717 转义字符的值是:"+s+" 长度是:"+s.length());
s = "\17178";
System.out.println("\\17178 转义字符的值是:"+s+" 长度是:"+s.length());
运行结果:
\17 转义字符的值是: 长度是:1
\171 转义字符的值是:y 长度是:1
\1717 转义字符的值是:y7 长度是:2
\17178 转义字符的值是:y78 长度是:3
“\17” 经转义得到一个特殊字符 “” 。而“\171” 转义后也得到一个字符 “y”。但 “\1717”、“\17178” 得到的字符串大于1,不再是一个字符,分别是 “y7”、“y78”。
也就是说,“\1717” 字符串只转义了“\171” 部分,再链接 “7” 部分。“\17178” 字符串只转义了 “\171” 部分,再连接 “78”。
那为什么 “\171” 为什么不转义 ”\17“ 部分,再链接 ”1“ 呢,而是作为一个整体进行转义的字符串呢?
这就是 ”贪心规则“ 所决定的。八进制的转义字符的取值范围是 \0~\377。所以解析 ”\171“ 字符串时,编译器尽可能多地结合字符成一个转移字符,”\171“ 还在取值范围内,所以就是一个字符。但 ”\1718” 字符串,最多结合前4个字符成一个有效的转义字符 “\171”,而“\1717” 已经超出取值范围,不是有效字符,所以最后解析成 “\171” + "7" 的结果。“17178” 也是如此。
总结:
- 编译器在分析字符时,会尽可能多地结合成有效字符,但有可能会出现语法错误。
- 贪心规则是有用的,特别编译器是对转义字符的处理。
java基础(二) 自增自减与贪心规则的更多相关文章
- 6.Java基础_Java自增自减/关系/逻辑/三元运算符
/* 自增自减运算符 关系运算符 逻辑运算符 三元运算符 (同C++) */ public class OperatorDemo01 { public static void main(String[ ...
- Java入土--Java基础(二)
Java基础(二) 接上一讲,我们接着来聊聊Java的一些基础知识,下一讲就会进行流程的控制. 类型转换 首先呢,是类型的转换,接上一个内容的数据类型,类型转换就是数据类型更进一步的应用. 由于Jav ...
- 【Java】【4】关于Java中的自增自减
摘要:理解j = j++与j = ++j的区别:正确用法:直接用j++,不要用前两种 正文: import java.util.*; public class Test{ public static ...
- Java面试题总结之Java基础(二)
Java面试题总结之Java基础(二) 1.写clone()方法时,通常都有一行代码,是什么? 答:super.clone(),他负责产生正确大小的空间,并逐位复制. 2.GC 是什么? 为什么要有G ...
- Java基础二(变量、运算符)
1.变量2.运算符 ###01变量概述 * A: 什么是变量? * a: 变量是一个内存中的小盒子(小容器),容器是什么?生活中也有很多容器,例如水杯是容器,用来装载水:你家里的大衣柜是容器,用来装载 ...
- Java基础(二) 基本类型数据类型、包装类及自动拆装箱
我们知道基本数据类型包括byte, short, int, long, float, double, char, boolean,对应的包装类分别是Byte, Short, Integer, Long ...
- java基础(二)-----java的三大特性之继承
在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句 ...
- [ 转载 ] Java基础二
前言 关于赢在面试的Java题系列基本收集整理完成了,所有题目都是经过精心挑选的,很基础又考验求职者的基本功,应该说被面试到的几率很大.这里整理挑选出来供大家面试前拿来看一看,所有题目整理自网络,有一 ...
- Java中的自增自减
情况①: for (int i = 0; i < 100; i++) { j = 1 + j++; } System.out.println(j); 结果是 0 !! 这是由于在进行后自增/自减 ...
随机推荐
- openerp学习笔记 视图样式(表格行颜色、按钮,字段只读、隐藏,按钮状态、类型、图标、权限,group边距,聚合[合计、平均],样式)
表格行颜色: <tree string="请假单列表" colors="red:state == 'refuse';blue:state = ...
- Spring Security构建Rest服务-0900-rememberMe记住我
Spring security记住我基本原理: 登录的时候,请求发送给过滤器UsernamePasswordAuthenticationFilter,当该过滤器认证成功后,会调用RememberMeS ...
- 如何使用Android Studio提高App质量
Android Studio作为现在谷歌主推的Android开发功能,除了提供了大量的功能帮助快速开发Android代码之外,在代码质量控制方面也提供了很多工具,这些工具都放在Analyze菜单下, ...
- JS - 解决鼠标单击、双击事件冲突问题(原生js实现)
由于鼠标双击时每一次触发双击事件都会引起两次单击事件和一次单击事件,原生的js不提供专门的双击事件. 因为业务原因,双击和单机都绑定了不同的业务,在双击的时候又触发了单机,影响了页面的正常显示 出现问 ...
- 简述C和C++的学习历程
总是被问到,如何学习C和C++才不茫然,才不是乱学,想了一下,这里给出一个总的回复. 一家之言,欢迎拍砖哈. 1.可以考虑先学习C. 大多数时候,我们学习语言的目的,不是为了成为一个语言专家,而是希望 ...
- sersync+rsync=实时异步备份
环境准备 服务器两台 rsync-server:192.168.1.8 (备份服务器) sersync-node1:192.168.1.9 (需要备份的服务器) 系统 CentOS7.4 关闭防火墙 ...
- python笔记05-----函数
函数 编程序语言中函数定义:函数是逻辑结构化和过程化的一种编程方法 def func(i): # def :定义函数的关键字:func:函数名:()内可以定义形参 i += 1 # 代码块或程序处理逻 ...
- eclipse中explorer显示方式
不知道是不是上面的描述.做个记录 project explorer 项目资源管理器 这个要打开代码目录需要再点开java resources 还会出现deployment Descriptor项目工程 ...
- CSS3设置Table奇数行和偶数行样式
table:.myTable tr:nth-child(even){ //偶数行 background:#fff;}.myTable tr:nth-child(odd){ //奇数行 backgrou ...
- Android Studio 打包生成 APK
1. 第一步 Build -> Generate Signed APK 2. 之后会要求开发者输入相关的密钥文件和密码 如果有则找到对应的 .jks 文件输入密码完成相应操作,否则则创建一个对应 ...