java创建对象
java虚拟机主要包括4个部分:
1.堆:存放对象。
2.栈:存放对象的引用。
3.静态区:存放被static修饰的变量。
4.常量区:存放常量。
对于一条创建对象的语句,如
1 | Car car = new Car(); |
首先在堆区新建了一个Car()对象,其次在栈区创建一个p引用,指向堆区新建的Car()。
通过类新建一个对象,在堆区都会新建一块区域。
但是如果使用下面的代码
1 | Car car1 = car; |
在栈区会新建一个car1的引用,而car1指向堆区的car(),也就是car和car1此时指向同一个的地址。
数据和字符串
通过new出来的对象都会放在堆,而java中有8大基本的数据类型,为了防止每次使用这些类型的时候都要消耗大量内存,这些数据的值都存在常量区中,比如
1 | int i = 2; |
2 | int j = 2; |
上述代码执行时,首先查看常量区是否存在2,不存在,开辟一块空间存放,在栈中开辟内存存放i并指向2,当需要创建j时,发现2已经存在,因此直接让j指向2。
但是如果用new的话,就不同了,如
1 | Integer a = new Integer(2); |
2 | Integer b = new Integer(2); |
执行代码时候首先在堆中开辟内存存放2,栈中a指向2,接着再在堆中开辟内存存放2,栈中b指向2。也就是说,使用new在堆中存放对象时,是不会检查对象是否重复的,堆只会新建。
String
String直接赋值的数据也是存放在常量区,因此其使用方式与基本数据类型是一样的,比如下面的代码
1 | String a = "abc"; |
2 | String b = a; |
3 | String c = "abc"; |
a,b,c三个变量指向的是同一个地址,此时如果改变a的值,令 a= “bcd”, 会在常量区新开辟一个空间存放bcd,a指向bcd,而b,c的指向的地址不变,仍然是abc。
而使用new String()创建的字符串对象存放在堆中,直接创建的字符串常量值存放在常量区,这两个地址是不同的,看下面的例子
1 | String a = new String("abc"); |
2 | String b = a; |
3 | String c = new String("abc"); |
此时堆中开辟了两个空间,分别存放“abc”,而b此时和a指向同一个地址。接着,我们令a=’abc’,此时a指向了常量区新开辟的地址, b指向堆区的1号abc,c指向堆区的2号abc。
值传递和引用传递
- 值传递:实参传递给形参的是值 ,形参和实参在内存上是两个独立的变量,对形参做任何修改不会影响实参。
- 引用传递:实参传递给形参的是参数对于堆内存上的引用地址,实参和形参在内存上指向 了同一块区域,对形参的修改会影响实参
我们先看例子
1 | public static void main(String[] args) { |
2 | testMain tm = new testMain(); |
3 | int a = 10; |
4 | int b = 20; |
5 | tm.change(a,b); |
6 | System.out.println("a:" + a + "\n" + "b:" + b); |
7 | |
8 | int[] a1 = {10}; |
9 | int[] b1 = {20}; |
10 | tm.change1(a1,b1); |
11 | System.out.println("a1:" + a1[0] + "\n" + "b1:" + b1[0]); |
12 | |
13 | } |
14 | public void change(int a, int b) { |
15 | int temp = b; |
16 | b = a; |
17 | a = b; |
18 | } |
19 | public void change1(int[] a, int[] b) { |
20 | int temp = b[0]; |
21 | b[0] = a[0]; |
22 | a[0] = temp; |
23 | } |
上面代码的输出
a:10
b:20
a1:20
b1:10
看完例子我们会以为,java在传常量的时候是值传递,而在传递数组,或者对象的时候是引用传递。
但是网上说的是,java只有值传递。更确切的说,java在从实参传递到函数的时候,是复制了一份实参的副本传递给形参。
先来分析第一个例子,在常量区开辟了两个空间,分别存放10和20,a和b指向了10和20,而在调用了change函数的时候,复制了a,b,假设是a’,b’,两个也分别指向常量区的10和20,而之后在函数里面的操作,就都是对a‘,b’的操作,并不会改变a,b的指向地址。
再分析第二个例子,同样在堆区开辟了两个空间存放数组,且a和b分别指向这两个空间,在调用change1函数后,复制出a‘,b’分别指向两个空间,但是,在函数里面是对a’指向的地址的数据进行操作,而不是a‘这个数据进行操作,也就是我们改变了a’指向的地址的内部数据导致了a指向的地址的内部数据改变,而a本身是没有改变了。好像有点绕口,总结一下就是虽然我们看到第二种方法改变了数据,但是它没有改变实参本身。
clone实现浅拷贝
java可以使用clone实现对话的浅拷贝,为什么说是浅拷贝,就是对象中但如果对象中存在如String类型,或者是数组类型的变量,clone并不会重新开辟一个空间,而是指向拷贝对象的空间,这个其实很像python中的浅拷贝。
java的浅拷贝需要类继承Clonable,并重写clone函数,具体类的实现如下
1 | public class Car implements Cloneable{ |
2 | ... |
3 | ... |
4 | ... |
5 | |
6 | |
7 | public Object clone() throws CloneNotSupportedException { |
8 | return (Car)super.clone(); |
9 | } |
10 | |
11 | } |
克隆的时候就可以这么写
1 | try{ |
2 | Car car = new Car(); |
3 | Car car_clone = (Car)car.clone(); |
4 | } |
5 | catch (Exception e){ |
6 | System.out.println(e.getStackTrace()); |
7 | } |
测试一下浅拷贝
1 | System.out.println(car.getName()==car_clone.getName()?"String类型不是额外开辟":"String类型是额外开辟"); |
2 | System.out.println(car.getSpeedHistory()==car_clone.getSpeedHistory()?"数组不是额外开辟":"数组是额外开辟"); |
我们会发现克隆出来的car_clone对于String和数组还是和car共享同一个空间,因此称为浅拷贝。