一.前言
(下文带“”的指针是在说Java,不带“”就是C++指针)
“Java没有指针”,这是我在上学期学习C++时候听到的。当时我其实没什么反应,毕竟还没接触过Java,只是被指针折磨的我心想Java一定好学很多吧(菜鸡的想法).....
直到这个假期我开始自己学习Java(C++也还在学,只是因为一些原因所以得两个一起学),专门先去看了下目录发现真的没有“指针”这两个字。但在接触过程中,发现貌似有和指针很相似的东西。然后这个学期学的数据结构与算法还是在用C++写的,在学到链表那一块时,因为链表的各个节点不一定是连续,所以要去到下一个节点的话是需要用到指针指向下一个节点的地址值,这个时候就需要用到指针,来获取地址值。这时,我就在想了,那java没有指针的话,岂不是没法做链表了嘛。但难道Java中真的没有“指针”吗?(点个题哈)
二.C++和Java实现的链表(部分)
大概介绍一下链表:①链表是以节点的方式来储存,是链式存储 ②节点由数据域(你要存储的数据)和指针域(指向下个结点的地址)构成 ③链表的各个节点不一定是连续存放的 ④链表分带头结点(头节点为空)的链表,根据实际的需求来确定。
1)首先我们看一段C++实现单链表的部分代码
//设置节点
通过上面代码我们可以发现,当我们想要去到下一个节点时候,就需要xx->next来获取指针域,但在Java中没有指针,怎么指向下一个节点呢?
2)下面是Java实现链表的Node类
public
就这样?对就这样,当你想要指向下一个节点时只需要xx.next就ok了。可能有人会问这是为什么捏?人家C++是有指针才能获取指针域,这Java没指针为啥还阔以?这是因为Java中其实隐藏着“指针”。
三.Java中的“指针”
先回顾一下C++中的指针的作用:通过指针间接访问内存,记录地址(指针就是一个地址)
1)也就是说指针是用来获取地址的,那Java既然没有“指针”,那岂不是获取不了地址了?没有地址是不可能的;所以不是说Java没有“指针”,而是Java用了其他的办法避开了“指针”。而这个方法就是Java中的引用。
Java当中的数据类型只有两种,基本类型、引用类型。除了八种基本类型(int、double、float、char等),剩下的全都是引用类型。而Java的引用实际上是对“指针”的一个封装。我们来做个比较。
2)就拿上面的列表来举例子。当写完链表后,肯定是要测试下链表的功能的,在测试添加功能时,我们首先要先实例化一个节点。
Node
上面是C++实现,下面是Java实现
Node node = new Node(1,"barry");
看下上面两段代码,十分相似,只是C++在实现时在前面加了一个*号表指针,而Java就是一个引用类型,但它们所实现的结果都是一样的。
①Java引用里,会先将对象的引用(也就是Node node)存到栈区里,然后new出来的东西就放入堆中(在堆中开辟了一块内存空间),这块空间会储存Node类中的成员变量以及成员方法的地址值,并且此空间还会产生一个地址值,通过等号又会将此地址值赋值给栈中的node。
②而在C++里,先在栈中存指针node,再向堆中申请内存空间存该指针所指向的函数变量,然后node指向该地址。
两者在内存中的操作几乎没有什么不一样,其实,java的引用在本质上就是一个“指针”,只不过对“指针”进行了封装,使其不能直接对内存进行操作,而且java的引用也只能指向对象,而不能指向基本数据类型。还有个很明显的例子就是Java中的数组,每一次new的时候都会开辟一块新的空间,和C++的指针数组相似,int* a = new int[5]; 先在栈中存储指针a,在堆中新的空间里存储该指针指向的元素,然后a指向该地址。
3)此时我们再来说下上面链表中的xx->next和xx.next。C++里在设置节点中Node* next,其实可以把它理解为,每次->next的时候实际上是在向堆中申请一块内存,然后当你new的时候就会实例化一个新的对象,开辟了一块新的地址区,然后将新的地址传给->next。 Java中其实也是同理,只是前者用的是指针,后者叫引用。或者可以把C++中 xx->next 看作 (*xx).next,这样看的话Java中的引用就相当于解引用了的指针。[1]
四.补充(Java引用和C++引用)
Java引用和C++引用是两个完全不同的概念。上文也说了Java的引用是指向一个对象,它是占一个新内存的;而C++的引用则是指向同一个内存的另外一个名字,也就是常说的别名。
1)首先看一个Java中直接传递引用的代码
public
可以看出实参并没有任何变化,其实直接传递引用和直接传递一个指针是一样的,传递进去的只是实参的一个拷贝值,赋值运算符只是让引用指向了一个新对象,其本身是没有任何影响的。其实也就是num1此时指向的是一个新的地址(这块地址存的是10),而num指向的还是原来那块地址(还是1)。(如果想要传进去后有改变的话,就得提供一个改变自身的方法,例如上例中可以在Number类里设置一个Setter的方法)
2)而C++中的引用,学过C++的肯定都会看过那个经典的swap例子(我这就不放出来了),C++引用它传的实际上是实参的本身。很多地方在介绍C++引用时,都会称它为别名。别名是什么,就假如你本来就有个名字,但你的朋友或亲人可能会给你起一个好叫的别名,但你还是你,不是说起了别名你就变了一个人了。所以说C++中的引用传递的是它的本身,而不是拷贝值。所以其实参也会发生改变。java的引用相当于C++中的指针,而C++的引用它其实可以理解为一个指针常量。所以千万不要将这两个引用混为一谈。
五.总结
所以Java中到底有没有指针呢?(再次点题,首尾呼应)你可以说它有,也可以说没有。Java确实没有“指针”这两个字,但是它是有指针的这种思想的——引用。感觉引用有那么点像稍许弱化的指针,但其实各有各的好。
----2020.7.13 再做一些补充----
最近又重新浏览了一下这篇文章,觉得在对比java引用和C++引用那里有点小问题。在Java引用那块觉得可能会让人有些小误会。先上一段代码:
//Value类
上面一段代码就是传一个Value的引用到swap方法中,然后对num1,num2进行交换的操作。按照我们上面所讲的,java中传递引用只是一个拷贝值,并不是引用本身,所以实参是不会有变化的。那我们来看下上面代码的结果:
我们可以看到,调用swap方法后,不论是在swap方法中还是在main中num1和num2都进行了交换。也就是实参发生了改变,这时可能就会有同学有疑惑了,上面不是说了传递引用只是传递一个拷贝值,为什么这里它的实参也改变了呢?其实,上面的例子中,传递给swap的确是拷贝值,是一个地址的拷贝指。前面也有说到过,java中每一次new后,都会在堆中开辟一个新的内存,并且将产生的地址值赋给它的引用,然后引用通过这个地址值对堆中的数据进行操作。所以这里调用swap方法后,传递了一个拷贝地址值给swap的引用,这时这个引用就能拿着这个地址值对堆中的数据进行操作了。我们通过几张图来理解下
首先在main中,通过Value value = new Value(); 我们可以让value引用拿到对象的地址值,此时value引用就可以拿着地址值对堆区的对象进行操作。这时通过value.num1 = 10; value.num2 = 20;给num1,num2附上值。接着就是调用swap方法...
调用swap方法,swap(value) 此时我们就会将main中value引用的地址拷贝值传过去,swap获取到了地址值后,它也可以对刚刚在main中new出来的那个Value对象进行操作,因为现在swap中的value引用也拿到了对应的地址值。这时候通过交换,就可以看到我们上面的结果。
讲了这么多,可能还是会有同学觉得这个解释很虚,因为我们又不知道它底层到底是怎么运作的,感觉就像是强行解释一波。不知道底层如何运作没关系,我们通过代码来测试一下,证明这里传递的只是一个地址拷贝值而并不是引用的本身。
static void swap(Value value) {
int temp = value.num1;
value.num1 = value.num2;
value.num2 = temp;
System.out.println("========swap========");
System.out.println("num1 = " + value.num1 + "," + "num2 = " + value.num2);
System.out.println("====================");
//
value = null;
}
我现在在swap函数中加了一句value = null; 如果我们在main中传递的是引用本身的话,那么我此时在swap中制空value对main中的value也会有影响,会引发空指针异常。我们来看下运行结果:
通过结果我们可以看到并没有任何的变化,也就是说实参根本没受到影响,这也就证实了java中传递引用只是拷贝值(地址值也是值),而并不是引用本身。
鄙人才疏学浅,若有不足劳烦指出,谢谢。
参考
- ^2020/4/25稍作修改