JAVA面向对象 对象/引用

本页面更新日期: 2016年07月17日

对象/引用

在前面 PersonTest.java 代码中, 有这样一行代码: Person p = new Person();

这行代码创建了一个 Person 实例, 也被称为 Person对象,这个Person对象被赋给 p 变量.

这行代码实际产生了两个东西: 一个是 p 变量, 一个是 Person 对象.

从 Person 类定义来看, Person 对象应包含两个实例变量, 而变量时需要内存来存储的.

因此, 当创建 Person 对象时, 有对应的内存来存储 Person 对象的实例变量.

画个图给你看看. (Person对象在内存中的存储示意图)

从上图可以看出, Person 对象由多块内存组成, 不同内存块分别存储了 Person 对象的不同成员变量.

当把这个 Person 对象复制给一个 引用变量 时, 系统如何处理呢?

难道系统会把这个 Person 对象在内存里重新复制一份嘛? 当然不会, Java才没有那么笨.

Java 让引用变量指向这个对象即可.

也就是说, 引用变量里存放的仅仅是一个引用, 它指向实际的对象.

与之前介绍的数组类型类似, 类也是一种引用数据类型.

因此程序中定义的 Person 类型的变量实际上时一个引用, 它被存放在栈内存里, 指向实际的 Person 对象.

而真正的 Person 对象则存放在 堆内存中.

再画个图给你看, 显示 Person 对象赋值给一个引用变量的示意图.

栈内存里的引用变量并未真正存储对象的成员变量.

对象的成员变量数据实际存放在堆内存里.

而引用变量只是指向该堆内存里的对象.

当一个对象被创建出来以后, 这个对象将保存在堆内存中.

Java程序不允许直接访问堆内存中的对象

只能通过该对象的引用来操作该对象.

也就是说, 不管时数组还是对象, 都只能通过引用来访问它们.

堆内存里的对象可以有多个引用.

即多个引用变量可以指向同一个对象. 例如下面代码:

//将 p 变量的值赋给 p2 变量
Person p2 = p;

上面这行代码把 p 变量的值赋给 p2 变量.

也就是将 p 变量保存的地址赋给 p2 变量.

这样 p2 变量 和 p 变量 将指向堆内存里同一个 Person 对象. 可以理解吧?

这样的话, 不管访问 p2 变量的成员变量和方法, 还是访问 p 变量的成员变量和方法.

它们实际上时访问同一个 Person 对象的成员变量和方法, 将会返回相同的访问结果.

小知识: 如果堆内存里的对象没有任何变量指向该对象, 那么程序将无法在访问该对象, 这个对象也就变成了垃圾, Java的垃圾回收机制会将该对象回收.

并且释放其所占用的内存.

因此, 如果希望让一个对象被回收, 只需要切断该对象的所有引用变量和它之间的关系即可. 也就是把这些引用变量的值赋为: null

对象的 this 引用

Java提供了一个 this 关键字, this 关键字总是指向调用该方法的对象.

根据this 出现位置的不同, this 作为对象的默认引用有两种情形.

  • 构造器中引用该构造器正在初始化的对象.
  • 在方法中引用调用该方法的对象.

this 关键字最大的作用就是让类中的一个方法, 访问该类里的另一个方法或实例变量.

我们假设定义了一个 Dog 类, 这个 Dog 对象的 run() 方法需要调用它的 jump() 方法.

那么应该如何做?

是否应该这样做?

public class Dog
{
    //定义 jump() 方法
    public void jump()
    {
        System.out.println("狗狗要跳跳");
    }
    //定义 run() 方法, run() 方法需要借助 jump()方法
    public void run()
    {
        Dog d = new Dog();
        d.jump();
        System.out.println("狗狗跑啊跑");
    }
}

使用这种方式来定义这个 Dog 类, 确实可以实现 run() 方法中调用 jump() 方法.

下面提供一个程序来创建 Dog 对象, 并调用该对象的 run() 方法

public class DogTest
{
    public static void main(String[] args)
    {
        //创建Dog对象
        Dog dog = new Dog();
        //调用Dog对象的 run() 方法
        dog.run();
    }
}

在上面程序中, 一共产生了两个 Dog 对象.

在 Dog 类的 run() 方法中, 程序创建了一个 Dog 对象.

并使名为 d 的引用变量来指向该 Dog 对象.

在 DogTest 的main() 方法中, 程序再次创建了一个 Dog 对象.

并使名为 dog 的引用变量来指向该 Dog 对象.

这里产生了两个问题:

  • 在run() 方法中调用 jump() 方法时是否一定需要一个 Dog 对象?
  • 是否一定需要重新创建一个 Dog 对象?

第一个问题的答案是肯定的, 因为没有使用 static 修饰的成员变量和方法 都必需使用对象来调用.

第二个问题的答案时否定的. 因为当程序调用 run() 方法时, 一定会提供一个 Dog 对象.

这样就可以直接使用这个已经存在的 Dog 对象, 无需重新创建 Dog 对象.

因此, 需要在 run() 方法中获得调用该方法的对象,通过 this 关键字就可以满足这个需求.

this可以代表任何对象, 当this出现在某个方法体中时, 它所代表的对象是不确定的.

但它的类型是确定的, 它所代表的对象只能是当前类.

只有当这个方法被调用是, 它所代表的对象才被确定下来.

谁在调用这个方法, this 就代表谁.

所以, 将前面的 Dog 类的 run() 方法改为如下形式更合适:

//定义一个 run() 方法, run() 方法需要借助 jump() 方法
public void run()
{
    //使用 this 引用调用run() 方法的对象
    this.jump();
    System.out.println("狗狗跑啊跑");
}   

上面的代码更好, 当一个 Dog 对象调用 run() 方法时, run() 方法需要依赖它自己的 jump() 方法.

在现实中, 对象的一个方法依赖于另一个方法的情形很常见:

  • 吃饭方法依赖于拿筷子方法
  • 写程序方法依赖于敲键盘方法

这种依赖都是同一个对象两个方法之间的依赖.

因此, Java允许对象的一个成员直接调用另一个成员. 可以省略 this 前缀.

也就是说, 将上面的 run() 方法改为如下形式也完全正确.


public void run()
{
    jump();
    System.out.println("狗狗跑啊跑");
}

大部分时候,一个方法访问该类中定义的其它方法/成员变量时不加 this 前缀的效果是完全一样的.

对于 static 修饰的方法而言, 则可以使用类来直接调用该方法.

如果在 static 修饰的方法中使用 this 关键字, 则这个关键字就无法指向合适的对象.

所以, static 修饰的方法中不能使用this引用.

由于 static 修饰的方法不能使用 this 引用, 所以 static 修饰的方法不能访问不使用 static 修饰的普通成员.

因此, Java 语法规定: 静态成员不能直接访问非静态成员.

下面写个代码演示一下这种错误用法:

public class StaticAccessNonStatic
{
    public void info()
    {
        System.out.println("简单的info方法");
    }
    public static void main(String[] args)
    {
        //因为 main() 方法是静态方法, 而 info() 是非静态方法
        //调用 main() 方法的是类本身, 而不是类的实例
        //因此省略的 this 无法指向有效的对象(你加上 this 也不行)
        info();
    }
}

上面的错误因为 info() 方法是属于 实例的方法. 而不是属于类的方法.

因此必需使用对象来调用该方法.

在上面的 main() 方法中直接调用 info() 方法时, 系统相当于使用 this 作为该方法的调用者.

而 main() 方法是一个 static 修饰的方法, static 修饰的方法属于类, 而不属于对象.

因此调用 static 修饰的方法的主调总是类本身.

如果允许在 static 修饰的方法中出现 this 引用, 那将导致 this 无法引用有效的对象, 因此上面的程序出现编译错误.

如果你混淆了这个概念, 最简单的解决方法就是你以后写程序, static 修饰的成员你都用类来调用, 不要用实例来调用. 你能懂不?

如果你确实需要在静态方法中访问一个非静态方法.

则只能重新创建一个对象.

例如上面的 info() 调用改为如下形式:

new StaticAccessNonStatic().info();

大部分时候, 普通方法访问其他方法/成员变量时 无需使用 this 前缀.

但如果方法里有个 局部变量和成员变量重名

程序又需要在该方法里访问这个被覆盖的成员变量, 则必需使用 this 前缀.

关于局部变量覆盖成员变量这个知识, 以后会说.

除此之外, this 引用也可以用于构造器中作为默认引用.

由于构造器是直接使用 new 关键字来调用, 而不是使用对象来调用.

所以 this 在构造器中代表该构造器正在初始化的对象.

public class ThisInConstructor
{
    //定义一个名为 foo 的成员变量
    public int foo;

    public ThisInConstructor()
    {
        //在构造器里定义一个 foo 变量
        int foo = 0;
        //使用 this 代表该构造器正在初始化的对象
        //下面代码将会把该构造器正在初始化的对象的 foo 成员变量设为 6
        this.foo = 6;
    }

    public static void main(String[] args)
    {
        //所有使用 ThisInConstructor 创建的对象的 foo 成员变量
        //都将被设为6, 所以下面代码将输出 6
        System.out.println(new ThisInConstructor().foo);
    }
}

在 ThisInConstructor 构造器中使用 this 引用时, this 总是引用该构造器正在初始化的对象.

this.foo = 6 这句话将正在执行初始化的 ThisInConstructor 对象的 foo 成员变量设为6.

这意味着该构造器返回的所有对象的 foo 成员变量都等于 6

与普通方法类似的是, 大部分时候, 在构造器中访问其他成员变量和方法时都可以省略 this 前缀.

但如果构造器中有一个与成员变量同名的局部变量, 又必需在构造器中访问这个被覆盖的成员变量, 则必需使用 this 前缀.

当 this 作为对象的默认引用使用时, 程序可以像访问普通引用变量一样来访问这个 this 引用.

甚至可以把 this 当成普通方法的返回值.

写个程序看看

public class ReturnThis
{
    public int age;
    public ReturnThis grow()
    {
        age++;
        //return this 返回调用该方法的对象
        return this;
    }
    public static void main(String[] args)
    {
        ReturnThis rt = new ReturnThis();
        //可以连续调用同一个方法
        rt.grow()
            .grow()
            .grow();
        System.out.println("rt 的age成员变量的值是:" + rt.age);
    }
}

从上面的程序可以看出, 如果在某个方法中把 this 作为返回值. 则可以多次连续调用同一个方法.

从而使代码更加简洁.

但是, 这种把 this 作为返回值的方法可能造成实际意义的模糊, 例如上面的 grow 方法, 用于表示对象的生长, 即 age 成员变量的值加 1 . 实际上不应该有返回值.

时间: 2024-08-07 16:56:49

JAVA面向对象 对象/引用的相关文章

Java面向对象-对象的多态性

Java面向对象-对象的多态性 Java中的多态性表现: 1,方法的重载和重写(覆盖): 2,可以用父类的引用指向子类的具体实现,而且可以随时更换为其他子类的具体实现: 我们先搞个父类Animal: 1 package com.java1234.chap03.sec13; 2 3 public class Animal { 4 5 public void say(){ 6 System.out.println("我是一个动物"); 7 } 8 } 再搞两个子类,分别是Dog和Cat类,

Java面向对象-对象和类概述

java语言提供了定义类.成员变量.方法等基本功能. 类可以认为是一种自定义的数据类型,可以使用类来定义变量,所有使用类来定义的变量都是引用变量,它们都会引用到类的对象. java面向对象的三大特征:封装.继承.多态. 封装:提供了private.protected和public三个访问修饰符来实现 继承:extends关键字让子类继承父类 多态:父类引用指向子类对象,使用继承关系来实现复用时,子类对象可以直接赋给父类变量,这个变量具有多态性,编程更加灵活.(百度搜:花木兰替父从军引例) 类和对

java面向对象---对象容器

泛型类--ArrayList<>; 2.对象数组中的每个元素都是对象的管理者而并非对象本身!!!!! 3.java类的基本数据类型 基本数据类型 包装类 byte Byte short Short int Integer long Long float Float double Double char Character boolean Boolean 4.for-each循环在对象数组中的应用 在基本类型的数组中,比如 int[],使用for-each 循环可以遍历数组中的值,但是不能对数组

Java基于对象基础 基于对象和面向对象的区别(转)

Java基于对象基础 基于对象和面向对象的区别 JavaScript设计者想把javascript语言设计成基于对象(object-based)的语言,他想把这个与面向对象(object-oriented)语言区分开来.但是实际上,可以将基于对象看作是面向对象. 原型对象和类的区别 在JavaScript中没有类这一可以使用关键字,当然保留字不算.所以它有自己对类这种封装结构的实现,这和Java,c++还是有很大区别的.但是我们还是能将原型对象(prototype object)看做是类,它们的

Java面向对象-方法的值传递和引用传递

Java面向对象-方法的值传递和引用传递 方法的值传递: 值传递 在方法里改变变量的值 作用范围仅仅是方法里 对外面不影响: 上代码: 1 package com.java1234.chap03.sec03; 2 3 public class Person { 4 5 void speak(int age){ 6 System.out.println("我今年"+age+"岁了"); 7 age=24; // 作用范围是方法里 8 } 9 10 public sta

Java面向对象——类和对象

java面向对象的三大特征:封装.继承和多态. 类与对象 所有类是引用数据类型. 对于一个类定义而言,包含三种最常见的成员:构造器.属性.方法,三 种成员可以定义多个或零个,当为零个时,定义的是一个空类. 类里面各成员之间的定义顺序没有任何影响,各成员之间可以相互调用. 一.定义类 1,定义类的语法: [修饰符] class 类名 { 零个到多个构造器定义... 零个到多个属性... 零个到多个方法... } 修饰符:可以是public.final等 类名:是合法的标识符即可,但从程序的可读性方

Java中对象、对象引用、堆、栈、值传递以及引用传递的详细解释

Java中对象.对象引用.堆.栈.值传递以及引用传递的详细解释 1.对象和对象引用的区别: (1).对象: 万物皆对象.对象是类的实例.在Java中new是用来在堆上创建对象用的.一个对象可以被多个引用所指向. (2).对象引用: 类似于C++中的地址.通过对象引用可以找到对象.一个引用可以指向多个对象.操纵的标识符实际上是指向对象的引用. 就像:对象存放在屋子里,对象的引用就相当于屋子的钥匙. 2.值传递和引用传递的区别: (1).值传递:传递的是值的拷贝.也就是说传递后就不互相关了. (2)

Java面向对象-类与对象

Java面向对象-类与对象 类与对象的关系 我们通俗的举个例子,比如人类是一种类,张三这个人就是人类的具体的一个个体,也就是java中的对象:这就是一个类与对象的关系: 类的定义 下面看实例 类的创建和使用 看下面实例 我们在com.java1234.chap03.sec01包下新建一个Person类 1 package com.java1234.chap03.sec01; 2 3 /** 4 * Person类 文件名和类名必须一致 5 * @author user 6 * 7 */ 8 pu

java面向对象编程(1)-类与对象

1.问题的提出      张老太养了两只猫猫:一只名字叫小白,今年3岁,白色.还有一只叫小花,今年100岁,花色.请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,年龄,颜色.如果用户输入的小猫名错误,则显示张老太没有这只猫. //用前面学习过的知识写出代码如下: public class Demo107{ public static void main(String []args){ int a=49;//输入的名字49,50 int cat1age=3; //第一只猫 String