08.创建和使用类
创建对象
Car myCar = new Car();
这一行代码包含了三个部分:
Car myCar
表示声明了一个Car
类型的变量myCar
,即myCar
是一个引用类型变量new
关键字表示创建一个对象Car()
则是构造器的调用,对创建的对象进行初始化
每次用new
创建一个对象,就会在堆中分配新的内存来保存新的对象信息,而myCar
这个引用变量本身则存储在栈中。
堆和栈的区别
-
在方法中定义的基本类型变量和引用类型变量,其内存分配在栈上,变量出了作用域(即定义变量的代码块)就会自动释放
-
堆内存主要作用是存放运行时通过
new
操作创建的对象
下面这张图展示了Car myCar = new Car();
这行代码运行时的内存状态:
图中0x6E34
是我们假设的内存地址。myCar
作为一个引用类型变量保存在栈中,你可以直观地认为myCar
变量保存的就是所创建对象在堆中的地址0x6E34
,即myCar
引用了一个对象,这正是引用类型变量这个叫法的原因;而堆中则保存着的对象本身,包含了其成员变量,如speed
、color
和engine
。
如果成员变量没有在构造器中初始化,则会是默认值。speed
和color
是int
基本类型,默认值为0
;engine
为引用类型,默认值为null
,即不引用任何对象
一个对象的成员变量,如果是引用类型的变量的话,比如engine
,则该成员变量可以引用到堆中的其它对象。 如下代码:
Engine engine = new Engine(180); Car myCar = new Car(0xffffff, 100, engine);
此时内存状态如下:
堆中的对象如果没有任何变量引用它们时,Java就会适时地通过垃圾回收机制释放这些对象占据的内存。你可以认为没有任何引用的对象(即没有任何引用类型的变量指向它),这个对象就成为"垃圾",Java虚拟机就会清理它们,为将来要创建的对象腾出空间。
引用类型和基本类型的区别
int color = 0; int speed = 100; Car myCar = new Car(color, speed);
则内存状态如下:
与引用类型myCar
不同,基本类型变量的值就是存储在栈中,作用域结束(比如main
方法执行结束)则这些变量占据的栈内存会自动释放。
访问对象属性
在类的内部可以访问自身的属性,在类的内部可以通过this
来访问自身的属性。
在外部(即其它类中)也可以访问一个类的非private
属性,通过对象名.
属性名的方式进行访问。
访问对象方法
方法可以在一个类内部进行调用
void run() { this.startup(); System.out.println("前进,速度为:" + speed); }
即在类的内部可以通过this
来访问自身的方法。
在外部(即其它类中)也可以访问一个类的非private
方法,通过对象名.
方法名的方式进行访问。
方法的返回和参数
可见调用方法时,传入的实参可以为字面量、变量或者表达式。
int e = calculator.add(c, c * d);
int c = calculator.add(1, 3);
public int add(int a, int b);
方法的调用过程
以int e = calculator.add(c, c * d);
为例,当程序执行到这一行代码时,会跳转到add()
方法的内部执行:
a
和b
就类似于在add()
方法内定义的两个局部变量,它们是main()
方法中c
和c * d
的值的拷贝;add()
执行完之后,a
和b
两个形参专用的内存就会被释放掉,同时会返回main()
方法继续执行其接下来的代码。
基本类型参数
传参即是实参的值赋给形参。对于基本类型的形参,在方法内部对形参的修改只会局限在方法内部,不会影响实参。
比如,给Calculator
增加一个increase(int)
方法:
1 基本类型参数 2 传参即是实参的值赋给形参。对于基本类型的形参,在方法内部对形参的修改只会局限在方法内部,不会影响实参。 3 4 比如,给Calculator增加一个increase(int)方法: 5 6 class Calculator { 7 public int add(int a, int b) { 8 return a + b; 9 } 10 11 public int increase(int a) { 12 return ++a; 13 } 14 public static void main(String args[]) { 15 Calculator calculator = new Calculator(); 16 int x = 10; 17 int y = calculator.increase(x); 18 System.out.println(x); 19 } 20 }
increase(int a)
方法定义了一个int
形参a
,将x
作为实参传入,虽然方法内部做了自增操作,但是并不会改变x
的值。因此,打印出来的x
的值是10
而不是11
。
引用类型参数
引用类型的实参传入方法中时,是将对象的引用传入,而非对象本身。因此,在方法执行时,实参和形参会引用到同一个对象。
在方法结束时,形参占据的内存虽然会被释放,但是通过形参对对象进行的修改则不会丢失,因为对象依然保存在堆中。
例如,Car
的构造器中如果对engine
进行修改:
1 public Car(int color, int speed, Engine engine) { 2 this.color = color; 3 this.speed = speed; 4 engine.power = 200; // 这里讲engine的power赋值为200 5 this.engine = engine; 6 }
则在main
方法中执行如下代码
1 Engine myEngine = new Engine(180); 2 Car myCar = new Car(0xffffff, 100, myEngine); 3 System.out.println(myEngine.power);
在Car myCar = new Car(0xffffff, 100, myEngine);
这行代码中,我们将myEngine
作为实参传递给了Car
的构造器,由于构造器中的engine
形参此时和myEngine
指向同一个对象,因此执行完构造器后,myEngine
的power
值会从180
变成200
。
再来考虑另外一种情况:
1 public Car(int color, int speed, Engine engine) { 2 this.color = color; 3 this.speed = speed; 4 engine = null; // 这里将engine设置为null 5 }
myEngine
传入到这个构造器中执行后,myEngine
是否会变为null
呢? 答案是否定的。虽然实参指向的对象可以在方法调用时被修改,但是实参本身的值(你可以认为是实参引用的对象地址)不会发生改变。
初始化成员变量
成员变量直接赋值:
private String title = "默认标题";
通过final方法赋值(无法被重写的特点,就完成了初始化的任务)
1 private String content = initContent(); 2 3 private final String initContent() { 4 return "默认内容"; 5 }
通过构造块初始化
还有一种方式是通过初始化构造块来,编译器会将初始化构造块的代码会自动插入到在每个构造器中,优先于构造器执行。例如:
1 class Post { 2 private long id; 3 private String title; 4 private String content; 5 6 { 7 title = "默认标题"; 8 content = "默认内容"; 9 } 10 11 public Post() { 12 } 13 14 public Post(long id) { 15 this.id = id; 16 } 17 }
这种情况下Post
的两个构造器内部虽然没有初始化title
和content
,但是由于构造块的存在,这两个成员变量会进行赋值。构造块可用于多个构造器复用代码(其优点)。
final关键字
但总体上来说,它指的是“这是不可变的”。
final关键字是我们经常使用的关键字之一,它的用法有很多,但是并不是每一种用法都值得我们去广泛使用。它的主要用法有以下四种:
- 用来修饰数据,包括成员变量和局部变量,该变量只能被赋值一次且它的值无法被改变。但其引用的对象的值是可以改变的,见下图。对于成员变量来讲,我们必须在声明时或者构造方法中对它赋值;
- 用来修饰方法参数,表示在变量的生存期中它的值不能被改变;
- 修饰方法,表示该方法无法被重写;
- 修饰类,表示该类无法被继承。
上面的四种方法中,第三种和第四种方法需要谨慎使用,因为在大多数情况下,如果是仅仅为了一点设计上的考虑,我们并不需要使用final来修饰方法和类。