创建对象
我们此前已经多次创建对象了,现在我们来进一步深入创建对象的知识。
假设我们已经如下定义了Car和Engine类:
Car.java
package com.tianmaying;
public class Car{
private int color;
private int speed;
private Engine engine;
public Car(){
}
public Car(int color, int speed){
this.color = color;
this.speed = speed;
}
public Car(int color, int speed, Engine engine){
this.color = color;
this.speed = speed;
this.engine = engine;
}
void startup(){
System.out.println("启动!");
}
void run(){
startup();
System.out.println("前进,速度为:" + speed);
}
}
Engine.java
package com.tianmaying;
public class Engine{
public double power;
public Engine(double power){
this.power = power;
}
}
重新审视我们曾经编写的创建对象的代码:
Car myCar = new Car();
这一行代码包含了三个部分:
Car myCar表示声明了一个Car类型的变量myCar,即myCar是一个引用类型变量
new关键字表示创建一个对象
Car()则是构造器的调用,对创建的对象进行初始化
每次用new创建一个对象,就会在堆中分配新的内存来保存新的对象信息,而myCar这个引用变量本身则存储在栈中。
堆和栈的区别
堆和栈都是Java中常用的存储结构,都是内存中存放数据的地方:
在方法中定义的基本类型变量和引用类型变量,其内存分配在栈上,变量出了作用域(即定义变量的代码块)就会自动释放
堆内存主要作用是存放运行时通过new操作创建的对象
下面这张图展示了Car myCar = new Car();这行代码运行时的内存状态:
图中0x6E34是我们假设的内存地址。myCar作为一个引用类型变量保存在栈中,你可以直观地认为myCar变量保存的就是所创建对象在堆中的地址0x6E34,即myCar引用了一个对象,这正是引用类型变量这个叫法的原因;而堆中则保存着的对象本身,包含了其成员变量,如speed、color和engine。
如果成员变量没有在构造器中初始化,则会是默认值。speed和color是int基本类型,默认值为0;engine为引用类型,默认值为null,即不引用任何对象。
你可以创建多个对象,每个对象都会在堆中拥有自己单独的内存空间,例如:
Car myCar = new Car();
Car herCar = new Car();
此时内存状态如下:
一个对象的成员变量,如果是引用类型的变量的话,比如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方法执行结束)则这些变量占据的栈内存会自动释放。
访问对象属性
在类的内部可以访问自身的属性。例如:
void run(){
startup();
System.out.println("前进,速度为:" + speed);
}
在Car的方法run()中,访问了speed属性。
也可以这样写:
void run(){
startup();
System.out.println("前进,速度为:" + this.speed);
}
即在类的内部可以通过this来访问自身的属性。
在外部(即其它类中)也可以访问一个类的非private属性,通过对象名.属性名的方式进行访问。
例如将Car的color属性设置为public:
public class Car{
public int color;
// ...
}
如果我们定义一个Driver类,可以这样访问color属性:
public class Driver{
public static void main(String[] args){
Car car = new Car();
car.color = 0xffffff; // 修改color属性的值
int color = car.color; // 访问color属性的值,将其赋给其他变量
System.out.println(car.color); // 将color作为参数,打印
}
}
访问对象方法
方法可以在一个类内部进行调用,例如:
void run(){
startup();
System.out.println("前进,速度为:" + speed);
}
在Car的方法run()中,访问了start()方法。
也可以这样写:
void run(){
this.startup();
System.out.println("前进,速度为:" + speed);
}
即在类的内部可以通过this来访问自身的方法。
在外部(即其它类中)也可以访问一个类的非private方法,通过对象名.方法名的方式进行访问。
如果我们定义一个Driver类,可以这样访问run()方法:
public class Driver{
public static void main(String[] args){
Car car = new Car();
car.run(); // 访问car对象的run()方法
}
}
方法的返回和参数
具有某个返回类型的方法,其返回结果可以用赋值操作赋给该类型的变量。
也可以赋给该类型的子类型的变量,可以在学习到继承和接口之后再做了解
例如,我们定义一个Calculator类:
class Calculator{
public int add(int a, int b){
return a + b;
}
}
可以用如下方式调用add()方法:
class CalculatorTest{
public static void main(String args){
Calculator calculator = new Calculator();
int c = calculator.add(1, 3);
int d = 9;
int e = calculator.add(c, c * d);
System.out.println(c); // 输出 4
System.out.println(e); // 输出 40
}
}形参是方法定义中出现的参数,用于接收外部传入的变量:如public int add(int a, int b)中,a和b是形参
实参即方法调用时传入的实际参数:
int c = calculator.add(1, 3); 表示将字面常量1和3作为实参,将方法计算结果赋值给变量c。
int e = calculator.add(c, c * d); 表示将变量c和表达式c * d作为实参,将方法计算结果赋给变量e。
可见调用方法时,传入的实参可以为字面量、变量或者表达式。
方法的调用过程
以int e = calculator.add(c, c * d);为例,当程序执行到这一行代码时,会跳转到add()方法的内部执行:
a和b就类似于在add()方法内定义的两个局部变量,它们是main()方法中c和c * d的值的拷贝;
add()执行完之后,a和b两个形参专用的内存就会被释放掉,同时会返回main()方法继续执行其接下来的代码。
基本类型参数
传参即是实参的值赋给形参。对于基本类型的形参,在方法内部对形参的修改只会局限在方法内部,不会影响实参。
比如,给Calculator增加一个increase(int)方法:
class Calculator{
public int add(int a, int b){
return a + b;
}
public int increase(int a){
return ++a;
}
public static void main(String args[]){
Calculator calculator = new Calculator();
int x = 10;
int y = calculator.increase(x);
System.out.println(x);
}
}
increase(int a)方法定义了一个int形参a,将x作为实参传入,虽然方法内部做了自增操作,但是并不会改变x的值。因此,打印出来的x的值是10而不是11。
引用类型参数
引用类型的实参传入方法中时,是将对象的引用传入,而非对象本身。因此,在方法执行时,实参和形参会引用到同一个对象。
在方法结束时,形参占据的内存虽然会被释放,但是通过形参对对象进行的修改则不会丢失,因为对象依然保存在堆中。
例如,Car的构造器中如果对engine进行修改:
public Car(int color, int speed, Engine engine){
this.color = color;
this.speed = speed;
engine.power = 200; // 这里讲engine的power赋值为200
this.engine = engine;
}
则在main方法中执行如下代码
Engine myEngine = new Engine(180);
Car myCar = new Car(0xffffff, 100, myEngine);
System.out.println(myEngine.power);
在Car myCar = new Car(0xffffff, 100, myEngine);这行代码中,我们将myEngine作为实参传递给了Car的构造器,由于构造器中的engine形参此时和myEngine指向同一个对象,因此执行完构造器后,myEngine的power值会从180变成200。
再来考虑另外一种情况:
public Car(int color, int speed, Engine engine){
this.color = color;
this.speed = speed;
engine = null; // 这里将engine设置为null
}
myEngine传入到这个构造器中执行后,myEngine是否会变为null呢? 答案是否定的。虽然实参指向的对象可以在方法调用时被修改,但是实参本身的值(你可以认为是实参引用的对象地址)不会发生改变。
初始化成员变量
成员变量直接赋值
初始化成员变量,一般通过构造器完成。也可以直接给类的属性进行赋值,例如:
class Post{
private String title = "默认标题";
private String content = "默认内容";
}
通过final方法赋值
可以通过调用final修饰的方法来进行赋值,例如:
class Post{
private String title = "默认标题";
private String content = initContent();
private final String initContent(){
return "默认内容";
}
}
通过构造块初始化
还有一种方式是通过初始化构造块来,编译器会将初始化构造块的代码会自动插入到在每个构造器中。例如:
class Post{
private long id;
private String title;
private String content;
{
title = "默认标题";
content = "默认内容";
}
public Post(){
}
public Post(long id){
this.id = id;
}
}
这种情况下Post的两个构造器内部虽然没有初始化title和content,但是由于构造块的存在,这两个成员变量会进行赋值。构造块可用于多个构造器复用代码。