上一篇中简单谈了一下自己对Java的一些看法并起了一个头,现在继续总结java的相关语法。java语法总体上与C/C++一样,所以对于一个C/C++程序员来说,天生就能看懂Java代码。在学习java的时候,上手非常快。我感觉自己就是这样,看代码,了解其中一些重点或者易错点的时候发现,与C/C++里面基本类似,甚至很多东西不用刻意去记,好像自己本身就知道坑在哪。所以这里我想简单列举一下语法点,然后尝试用C/C++的视角来解读这些特性。
引用类型
引用中的指针与内存
上一次,我总结一下java中的数据类型,在里面提到,Java中有两大数据类型,分为基本数据类型和引用数据类型。并且说明了简单数据类型,这次就从引用数据类型说起;
引用数据类型一般有:数组、字符串、类对象、接口、lambda表达式;这次主要通过数组和字符串来说明它
引用数据类型在C/C++中对应指针或者引用。其实关注过我之前C/C++反汇编系列文章的朋友知道,在C/C++中引用实质上就是一个指针。所以这里我也将java中引用类型理解为指针。所以从本质上讲,引用类型都是分配在堆上的动态内存。然后使用一个指针指向这块内存。这是理解引用类型很重要的一点。
数组的定义如下
char[] a = new char[10];
char[] b = new char[] {'a', 'b', 'c'};
char[] c = {'a', 'b', 'c'};
其实这种形式更符合 变量类型 变量名 = 变量值
这种语法结构, char[]
就像是一种数据类型一样,char
表示数组中元素类型, []
表示这是一个数组类型
字符串的简单定义如下:
String s = "Hello world";
上面说到引用类型都是分配在堆上的。所以字符串和数组实质上都是new 出来的。即使有的写法上并没有new 这个关键字,但是虚拟机还是帮助我们进行了new 操作。有new就一定有delete了。那么哪里会delete呢?这些操作一般都由java的垃圾回收器来处理。我们只管分配。这就帮助程序员从资源回收的工作中解放出来了。这也是java相比于C/C++来说比较优秀的地方。有人可能会说C++中有智能指针,也有垃圾回收机制。确实是这样。但是我觉得还是有点不一样。java是天生就支持垃圾回收,就好像从娘胎生出来就有这个本能,而C/C++是由后天学会的,或者说要刻意的去进行操作。二者还是不一样的。
下面有这样一段代码:
char[] a = new char[10];
System.out.println(a);
我们打印这个a变量,发现它出现的是一个类似16进制数的一个东西。这个东西其实是一个地址的hash值,为什么不用原始值呢?我估计是因为有大神能够根据变量的内存地址进行逆向破解,所以这里为了安全对地址值进行了一个加密。或者为了彻底贯彻Java不操作内存的信念。(我这个推断不知道是不是真的,如果有误,请评论区大牛指正。)这也就证明了我之前说的,引用类型本质上是一个指针。
char[] a = new char[]{'a', 'b', 'c'};
char[] b = new char[]{'a', 'b', 'c'};
System.out.println(a);
System.out.println(b);
上面这段代码,我想学过C/C++的人应该一眼就能看出,这里打印出来的a和b应该是不同的值,这里创建了两块内存。只是内存中放的东西是一样的。
char[] a = new char[]{'a', 'b', 'c'};
char[] b = a;
b[0] = '0';
b[1] = '1';
b[2] = '2';
System.out.println(a);
System.out.println(b);
这里从C/C++的角度来看,也很容易理解:定义了两个引用类型的变量,a、b都指向同一块内存,不管通过a还是b来寻址并写内存,下次通过a、b访问对应内存的时候肯定会发现值与最先定义的不同。
String s = "hello";
System.out.println(s.hashCode());
s += "world";
System.out.println(s.hashCode());
由于Java不具备直接访问内存的能力,不能直接打印出它的内存地址,所以这里用hashCode 得到地址的hash值。通过打印结果说明这个时候s指向的地址已经变了。也就是说虽然可以实现字符串的拼接,但是虚拟机在计算得出拼接的结果后又分配了一块内存用来保存新的值。但是任然用s这个变量来存储地址值,用赵本山的话来说就是“大爷还是那个大爷,大妈已经不是原来的那个大妈了”。
也就是说Java分配内存的时候应该是按需分配,需要多少分配多少。不够就回收之前的,再重新按需分配。这就导致了java中字符串和数组的长度是不能改变的。
String s = "hello";
System.out.println(s.hashCode());
(s.toCharArray())[0] = 'H';
System.out.println(s.hashCode());
System.out.println(s); // 这里字符串的值不变
上述这段代码,通过toCharArray将字符串转化为char类型的数组,然后修改数组中的某一个元素的值,我原来以为这样做相当于在String所在内存中修改,最终打印s时会出现 "Hello" ,但是从结果上来看并没有出现这样的情况,s指向的地址确实没变,但是s也是没变的,那只能解释为toCharArray 又开辟了一块内存,将String中的值一一复制到数组中。
在学习中我尝试过各种数据类型强转
String s = "Hello World";
char[] a = (char[]) s;
int p = (int)s;
像这样的代码我发现并不能通过编译。在C/C++中,可以进行任意类型到整型或者指针类型的转化,常见的转化方式就是将变量所在地址进行赋值或者将变量对应的前四个字节进行转化作为int或者指针类型。但是在java中这点好像行不通。Java中强转好像只能在基本数据类型中实现,而在引用类型中通常由函数完成,并且完成时并不是简单的赋值,还涉及到新内存空间的分配问题。
越界访问
由于C/C++中提供了访问内存的能力,而且由于现代计算机的结构问题,C/C++中存在越界访问的问题,越界访问可以带来程序编写的灵活性,但是也带来的一些安全隐患。对于灵活性,相信学习过Windows或者Linux编程的朋友应该深有体会,系统许多数据结构的定义经常有这类:
struct s {
char c;
}
s *p = (s*)new char[100];
这样就简单的创建了一个字符串的结构。这里C变量只是提供了一个地址值,后续可以根据c的地址来访问它后续的内存。
安全问题就是大名鼎鼎的缓冲区溢出漏洞,我在相关博客中也详细谈到了缓冲区溢出漏洞的危害以及基本的利用方式。这里就不在赘述。
那么Java中针对这种问题是如何处理的呢? Java中由于不具有内存访问的能力,所以这里它简单记录当前对象的长度,只要访问超过这个长度,立马就会报异常,报一个越界访问的异常。(这里我暂时没有想到对应的java演示代码,所以简单说一下吧)
空指针访问
还记得C/C++指针中常见的一个NULL吧,既然Java中引用类型相当于一个指针,那么它肯定也存在空指针问题。在Java中空指针定义为null。如果直接访问null引用,一般会报空指针访问异常。
char[] c = null;
c[0] = 'A'; //异常
语句
关于引用类型我暂时了解了这么多东西。下面简单列举一下java中的运算符和相关语句结构
运算符
java中的运算符主要有下列几个:
- 算数运算符: + 、-、 *、 /、 %、 ++、 --、
- 赋值运算符: = 、+=、 -=、%=、/=、*=
- 比较运算符: ==、 >、 <、 >=、 <=、 !=
- 逻辑运算符: &&、 ||、 !、
- 三目运算符
- 位运算符: >>、 <<、 >>>(无符号右移)、 <<<、&、|、~
这些运算符用法、要点、执行顺序与C/C++中完全相同。所以这里简单列举。不做任何说明
语句结构
java中的顺序结构与其他语言中一样,主要有3种
- 顺序结构
- 判断结构: if、if...else、 if...else if...else if...else
- 循环结构:while、for、do while
用于与其他语言一样,这里需要注意的是,Java中需要判断的地方只能使用bool值作为判断条件,比如 5 == 3
、 a == 5
这样的。
在C/C++中有一条编程规范,像判断语句中将常量写在前面就像这样
if(5 == a){
//....
}
这样主要是为了防止将 ==
误写成 =
,因为在C/C++中 只要表达式的值不为0 就是真,像 if(a = 5)
这样的条件是恒成立的。而Java中规定判断条件必须是真或者假,并且规定boolean类型不能转化为其他类型,其他类型也不能转化为boolean,所以 if(a = 5)
这样的语句在Java中编译是不会通过的。