学习 Java类和对象——对象引用

为了完成本次任务,我们需要掌握:

对象的引用和对象;
对象的比较;
垃圾收集机制。

实战1.坐标系中两点距离计算

已知两个点 A、B 以及坐标分别为(2,3) 、(8,-5) ,求 A 和 B 两点之间的距离。

两点间的距离计算公式:设两个点A、B以及坐标分别为(x1,y1)、(x2,y2),则A和B两点之间的距离为:∣AB∣

/**
 * 任务:已知两个点 A、B 以及坐标分别为(2,3) 、(8,-5) ,求 A 和 B 两点之间的距离。
 * 类名为:Distance
 */

public class Distance {

    /**
     * 定义一个静态方法,该方法计算坐标两点的距离,携带四个参数,分别为x1、y1、x2、y2的值
     * 将距离结果返回,返回类型为double
     */
    public static double dist(double x1,double y1,double x2,double y2)
    {
        double dis = Math.sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
        return dis;
    }
    // 定义主方法
public static void main(String[] args)
{
    // 通过类名.方法名的方式调用计算两点间距离的方法,分别将A、B的x1、y1、x2、y2的值传入该方法中
    Distance distance = new Distance();
    double dis = distance.dist(2,3,8,-5);
    // 不换行输出,输出格式: A、B两点的距离为xx
    System.out.printf("A、B两点的距离为%f",dis);
}

}

输出结果:A、B两点的距离为10.000000

实战2.模拟手机功能

实现手机的基本功能。

手机具有属性:品牌(brand)、型号(type)、价格(price)、操作系统(os)和内存(memory);

具有功能:查看手机信息(about())、打电话(call(int number))、玩游戏(play())。

/**
 * 任务:实现手机的基本功能。
 * 类名为:Phone
 */

public class Phone {

    // 定义五个变量,分别表示品牌、型号、价格、操作系统和内存
    String brand;
    String type;
    String os;
    double price;
    int memory;
    // 无参构造
    public Phone()
    {

    }
    // 有参构造
    public Phone(String brand,String type,String os,double price,int memory)
    {
        this.brand = brand;
        this.type = type;
        this.os = os;
        this.price = price;
        this.memory = memory;
    }
    /**
     * 定义一个方法,该方法实现查询手机信息的方法,无返回值
     * 输出格式:品牌:xx
     *           型号:xx
     *           操作系统:xx
     *           价格:xx
     *           内存:xx
     * 中间用换行符隔开
     */
     public void about()
     {
        System.out.print("品牌:"+brand+"\n"+"型号:"+type+"\n"+"操作系统:"+os+"\n"+"价格:"+price+"\n"+"内存:"+memory+"\n");
        
     }
    /**
     * 定义一个方法,该方法实现打电话的功能,无返回值,
     * 携带一个int型参数,其中1,2,3分别表示爸爸、妈妈、姐姐的号,
     * 输出格式  如果参数为1,换行输出:正在给爸爸打电话
     * 如果出现其它情况,换行输出:你所拨打的电话为空号
     */
    public void call(int n)
    {
        if(n==1)
            System.out.println("正在给爸爸打电话");
        else if(n==2)
            System.out.println("正在给妈妈打电话");
        else if(n==3)
            System.out.println("正在给姐姐打电话");
        else 
                System.out.println("你所拨打的电话为空号");
    }
    /**
     * 定义一个方法,该方法实现听音乐的功能,无返回值
     * 携带一个参数,其表示为歌曲名
     * 不换行输出格式:正在播放xx
     */
    public void play(String str)
    {
        System.out.print("正在播放"+str);
    }
    // 定义主方法
public static void main(String[] args)
{
    // 通过无参构造创建手机对象
    Phone phone = new Phone();
    // 设置手机品牌为小米,型号为小米9,操作系统为Android 9,价格为2599,运行内存为8
    phone.brand = "小米";
    phone.type = "小米9";
    phone.os = "Android 9";
    phone.price = 2599;
    phone.memory = 8;
    // 查询手机信息
    phone.about();
    // 给妈妈拨打电话
    phone.call(2);
    // 播放浮夸这首歌
    phone.play("浮夸");
}

}

输出结果:

品牌:小米

型号:小米9

操作系统:Android 9

价格:2599.0

内存:8

正在给妈妈打电话

正在播放浮夸

相关知识:

对象的引用和对象
首先我们来看一个例子:

public class Demo{
   //默认构造方法  
    public Demo{ }
} 


接下来,我们用 Demo 类来创建一个对象。  

Demo demo=new Demo();  
我们来对这条语句进行一个解析:

右边的“new Demo”,是以 Demo 类为模板,在堆空间里创建一个 Demo 对象;
末尾的()意味着,在对象创建后,立即调用 Demo 类的构造函数,对刚生成的对象进行初始化;
左边的“Demo demo”创建了一个 Demo 类引用变量,它存放在栈空间中。也就是用来指向 Demo 对象的对象引用;
“=”操作符使对象引用指向刚创建的那个 Demo 对象。
当然这条语句我们也可以写成:  

Demo demo;     //创建对象引用,存储在栈内存
demo=/*将对象引用指向对象*/new Demo();     //创建对象,存储在堆内存  
对象与引用的关系:

一个对象引用可以指向一个对象;  

Demo demo;     //一个对象引用    
demo=new Demo();     //一个对象引用指向一个对象  
也可以不指向对象;  

Demo demo;     //创建对象引用,但是没有指向对象  
一个对象可以被一个对象引用;  

Demo demo;     //创建对象引用    
demo=new Demo();     //创建对象,并被一个对象引用指向  
也可以被多个对象引用同时引用。  

Demo demo1,demo2,demo3;     //创建多个对象引用    
demo1=new Demo();  
demo2=demo1;    
demo3=demo2;     //创建对象,并被多个对象引用指向    
引用只是存放一个对象的内存地址,并非存放一个对象,
就好比钥匙与房子的关系,钥匙相当于引用,房子相当于对象,钥匙只是能打开房子,而并非就是一个房子。创建一个引用对象就是创建一把钥匙而并非是造了一个房子。

对象的比较
在 Java 之中不仅仅存在两个数字与两个字符串之间的比较,还存在两个对象之间的比较。

众所周知,两个数字之间的比较我们使用“==”,两个字符串之间的比较我们使用“equals()”,那么两个对象之间如何进行比较呢?“==”比较的是两个对象引用的地址是否相等,而“equals()”比较的是两个对象引用的内容是否相等,而既然要进行两个对象之间的比较,那么就必须要实现两个对象之间所有属性内容的比较。

例子:  

public class Person {  
    private String name;  
    private int age;
    public Person(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }
    public boolean compare(Person per) {//此时有两个对象:this表示当前对象,另外一个是参数传递  
        if (this == per){  
             return true;//如果自己和自己比较  
        }  
        if (per == null){  
            return false;//若传入的为空  
        }  
        if (this.name.equals(per.name) && this.age == per.age) {//此时per对象已经在类的内部,可以直接利用  
            return true;  
        }  
        return false;  
    }
    public static void main(String args[]) {  
        Person perA = new Person("123", 20);  
        Person perB = new Person("123", 20);//将对象拥有的属性进行完整比对  
        if (perA.compare(perB)) {  
            System.out.print("两个对象相等!");  
        } else {  
            System.out.print("两个对象不相等!");  
        }  
    }  
}


执行结果:

两个对象相等!  


垃圾收集机制
自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用对象),则没有被任何指针给指向,因此占用的内存也可以被回收掉。

在用 C 之类的编程语言时,程序员需要自己手动分配和释放内存。而 Java 不一样,它有垃圾回收器,释放内存由回收器负责。本文接下来将介绍垃圾回收机制的基本过程。

既然我们要做垃圾回收,首先我们得搞清楚垃圾的定义是什么,哪些内存是需要回收的。

Java 中标记垃圾的算法主要有两种,引用计数法和可达性分析算法。我们首先来介绍引用计数法。

引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。

如果该对象被其它对象引用,则它的引用计数加 1,如果删除对该对象的引用,那么它的引用计数就减 1,当该对象的引用计数为 0 时,那么该对象就会被回收。当对象被赋值为 null 是,该对象的引用计数会变为 0 。

但是它有一个缺点:无法检测出循环引用的情况,导致内存泄露。

我们举一个简单的例子。

public class MyObject {  
    public MyObject childNode;  
}  
public class ReferenceCounterProblem {  
    public static void main(String[] args) {  
        MyObject object1 = new MyObject();  
        MyObject object2 = new MyObject();  
        object1.childNode = object2;  
        object2.childNode = object1;  
    }  
}  


从上述代码中我们可以看出,object1 和 object2 并没有任何价值,但是他们循环引用,造成内存泄露。

可达性分析算法是从离散数学中引入的,也是如今正在使用的策略,程序把所有的引用关系看作一张图(DOM 图类似),从一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,就如递归思想一般,遍历所有,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于 GC Root。

在 Java 语言中,可作为 GC Root 的对象包括以下 4 种:

虚拟机栈中的引用对象;

方法区中的常量引用对象;

方法区中的类静态属性引用对象;

本地方法栈中的引用对象。

在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。

标记-清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为 2 部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。清理掉的垃圾就变成未使用的内存区域,等待被再次使用。

这逻辑再清晰不过了,并且也很好操作,但它存在一个很大的问题,那就是内存碎片。当我们将垃圾回收完,内存会被切分成很多段。

我们知道开辟内存空间时,需要的是连续的内存区域,如果这时候我们需要一个 2M 的内存区域,其中有 2 个 1M 是没法用的。这样就导致,其实我们本身还有这么多的内存的,但却用不了。

复制算法(Copying)是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效。

但是它也很明显的暴露了另一个问题,假设我有一个 140 平米的房子,那么就只能当 70 平米的小两房来使,代价实在太高。

标记-整理算法(Mark-Compact)的标记过程仍然与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。

标记-整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。

看起来很美好,但它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。

分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述 3 种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。

对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。


 

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值