重学java(1)

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
    @Override
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共享同一个空间,因此称为浅拷贝。