Java再入:05 - 类和对象的创建

本文详细介绍了Java中类和对象的概念,包括内存模型、类的声明与对象的创建过程,以及对象的is-A和has-A关系。通过实例展示了类的继承、构造函数的使用,并探讨了对象创建的细节,如初始化、内存分配等。此外,还讨论了向上转型的原理及其限制。
摘要由CSDN通过智能技术生成

05 类和对象的创建

在Java的世界里,必须先有类,才能有对象,这视乎和设计模式有关,如果你有玩过JavaScript,你就会发现其中的明显的区别

一、内存模式

参考文章:

现代高级语言基本都是分为含有下列部分:

  • 静态存储区域:是由编译器自动分配和释放的,即内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,直到整个程序运行结束时才被释放,如全局变量与 static 变量
  • 栈:在函数内顺序执行的代码中,声明变量,变量所代表的内存,是由堆空间分配的。一般表现为声明语句的左侧。(也经常称其为函数栈)
  • 堆:称为动态内存,数据结构复杂

那Java里面是什么样的呢?

  • 方法区(class,static变量)
  • 栈 (变量)
  • 堆 (对象)

字符串常量呢?
Java 中方法区与常量池

  • 在 JDK6.0 及之前版本,字符串常量池是放在 Perm Gen 区(也就是方法区)中,此时常量池中存储的是对象。
  • 在 JDK7.0 版本,字符串常量池被移到了堆中了。此时常量池存储的就是引用了。在 JDK8.0 中,永久代(方法区)被元空间取代了。

在新版本中,方法区被元空间取代?
永久代被替换原因、元空间特点、元空间内存查看分析方法

二、类和对象的初步了解

(1) 什么是类

具有相同特性(数据元素)和行为(功能)的对象的抽象就是类(当然,这并不意味者,类要在对象之后才有,我们可以虚构类:比如古代神话的神,就是一种虚构的类)

(2) 什么是对象

对象是人们要进行研究的任何具体事物

(3) 鸡和蛋的问题

先有对象还是先有类?OOP的思想是通过将现实生活的事物抽象成一个个类,从而符合人类思维的编程逻辑,但是如果现实世界没有的事物呢?

我们想象需要一个怪兽类,它没有事物参照,通过假象描述它的特点,创建出的怪兽类。在通过new创建具体的怪兽。

事实上,OOP完全可以先有类,再进行实例化,并且Java也是这么实现的,没有凭空出现的对象,都是通过实例化Object类创建的。实例化也会检查是否加载该类

(4) 类和对象的关系

  1. 类是对象的模板,这就像生物中的DNA序列一样,要是这类物种你就应该包含这块DNA序列
  2. 该类的对象根据类模板创建生成,是类的实例
  3. 类是一种抽象,将一类事物的相同点抽离,收敛为类
  4. 可以对类继续抽象,即对类进行分类或者说扩展,关键词为extends,这种类叫做子类,也可叫做派生类;相对的被抽象的类叫做父类。
  5. Object是所有类的祖先类,任何一个类时候如果没有明确的继承一个父类的话,那么它就是Object的子类。
    从这一点上,假设直接定义一个类叫Person,抽象出子类Son;那么Object是Person的父类,Object是Son的超类,Son是Person和Object的派生类。
    从某种意义上说,Person是对Object的抽象,这样恰好的将整个逻辑连贯起来
  6. 实例 instanceof 类,判断实例是否属于该类

三、类的声明和对象的创建

初学者往往会把类和对象搞混淆,类是多个实例的抽象。在现实社会中,小时候你爱玩,你去商店玩具赛车。

老板问: 你要什么车车呀?(其实是问你要什么类型的赛车)

你说: 要遥控赛车。(遥控赛车是赛车的子类,对其的更加具体的抽象)

老板说: 遥控赛车的车是啥呀?怎么样的,包装(你就要描述遥控赛车这种车类的具体属性,还有操作,比如怎么操作,什么外型。做完这些你就声明了一个遥控赛车类)

你说: 老板来一款吧!(你发动了声明变量和new操作,要拿钱(你的栈内存和堆内存),买一辆遥控赛车(创建一个变量做遥控器、创建一个对象做车),通过遥控器让小车东起来(通过访问变量来访问这个对象))

(1) 通过上面的一小段真实情景,相信你已经了解类的创建和作用。

  1. 那我们尝试编写一个遥控赛车类、遥控器类:

    class RaeingCar extends Car {
      private String direction;
      public void move() {
        System.out.print("向" + this.direction + "移动");
      }
    }
    
    class Car{
      private String color;
      private String size;
      private String name;
      private String speed;
      public void move(direction) {
        System.out.print("向" + direction + "移动");
      }
    }
    
  2. 我们尝试操作赛车,但是我们需要一个入口方法

    public class Start() {
      public static void main() {
        const raeingCarTelecontrol = new RaeingCar();
        raeingCarTelecontrol.direction = "上";
        raeingCarTelecontrol.move();
      }
    }
    

(2) 构造函数 => 对象的构造和初始化

创建类的实例,必须调用构造函数,但其主要作用是初始化对象

要注意的点:

  1. 函数名和对象同名

  2. 无修饰符或者是public

  3. 无返回值类型且不能是void(普通的方法必须要返回类型,即使没有也需要返回void)

    class TEST {
      private String name;
      Test(String name) {
        this.name = name;
      }
    }
    
  4. 如果一个类没有显示声明构造函数,会执行默认的构造函数 className() {} 空的方法体

构造函数无论是外部还是类内部都无法被访问,它是类的一个特殊的成员

(3) 上面不是谁都知道吗,有没有更加具体,底层的过程呢?

下面细说一下,Java对象的创建过程:

大部分上面的博文已经提到的我就不说了,对一些进行补充:

  1. 创建对象,必须有类的声明的前提,自然会先检查类是否加载
  2. 创建对象,为其分配空间(分配前自然是扫描类的各个字段,方法永远是保存在类中即方法区,需要的时候绑定调用(多态)),初始化内存,各个字段被赋值初始值
  3. 创建对象先扫描父类,再扫描子类,new 子类的时候只会创建一个对象(如果成员变量有重名,该成员会添加父类名以示区分,不如Fanter.age和age都是对象的属性,并不会直接删除过滤
  4. 完成上面操作进行init,包括:变量初始化声明、语句块、构造函数执行。同样是由父到子。

三、is-A 和 has-A了解一下

  • is-A代表了 实例 instanceof 类 的关系
  • has-A代表了 实例.成员 的关系,成员包括:变量和方法

了解这个有什么作用?
判断是否正确编译和执行的依据

(1) 为什么向上转型能够成立? => 满足is-A的关系

你想象一下,就拿上面的遥控赛车讲解,假设我再在遥控赛车基础上
抽象出一个智能遥控赛车,比遥控赛车多了一个自动识别障碍物的功能,那你拿遥控赛车的遥控器能操作智能遥控赛车吗?
当然可以,应为智能遥控赛车已经包括了遥控赛车的功能(向上兼容),并且对遥控赛车的功能没有任何影响。

从上面其实体现了下面的思想:

  1. 子类是对父类功能的继承
  2. 子类的功能不会对父类功能产生影响(你可能说重写,不就改变了父类方法?方法的执行是动态绑定,我前面说过,如果出现相同的,会加以父类名称以便区分 => super.成员);所以可以任务子类无法对父类进行修改,只能对其扩展

第二点其实非常重要,这是让开发人员放心使用OOP开发的重要原因之一:
如果你定义了一个Person类并且有一个吃饭的方法,你定义了一个子类Son继承Person继承吃饭的方法,你再定义一个Sister类继承Person,如果Sister能够去除Person类的某些行为(比如:吃饭),那岂不是想想都害怕:Sister连最基本的吃饭都不需要了,那Sister还是Person吗?它连Person最基本的行为都失去了,显然不应该属于人,但是这和Sister类是继承Person而得来的相矛盾。

(2) 为什么向上转型能够成立无法访问子类对象特有的成员? => 不满足has-A的关系

在栈中申明的对象引用表示从某种角度(引用的类型)客观的看待这个对象,编译决定是否符合道理(以引用变量的声明类型参考),运行决定是否能够成功(以实际的对象类型参考)

再来看这段代码,你是否觉得完全合理

class Person {
  public void eat(String food) {
    System.out.print("人要吃饭");
  }
}

class Son extends Person {
  public void fuck() {
    System.out.print("男孩能做fuck 动作");
  }
  public void eat(String food) {
    System.out.print("男孩要吃一桶饭,还不够");
  }
}

class Sister extends Person {
  public void eat(String food) {
    System.out.print("女孩只要吃一粒饭");
  }
  public void befucked() {
    System.out.print("女孩能做fuck 动作");
  }
}

public class Start{
  public static void main() {
    Person son = new Son();
    Person sister = new Sister();
    son.eat("香蕉");
    sister.eat("香蕉");
    son.fuck();//编译错误
    sister.befucked();//编译错误
  }
}
  1. 对象引用变量son,它是将 new Son() 看成一个 Person 对象,这当然是合情合理的嘛,比如
    son.eat(),引用变量son要求对象做eat操作,它当然可以通过编译,对于该对象来说:我不禁继承了Person的eat的行为,我还更能吃了(重写eat),它当然会执行自己的eat了。
  2. son.fuck(),该对象对于引用变量来说,它是一个 Person 对象的啊,怎么能有 fuck 方法,这明显不符合道理,编译错误

(3) 总结

动态绑定

在调用该方法时会根据调用时传入的参数进行解析,匹配,选择相应的函数调用(函数在方法区)

对象数据类型转换

PS: Java 语言允许某个类型的引用变量引用子类的实例,而且可以对这个引用变量进行类型转换

  1. 如果把引用类型转换为子类类型,则称为向下转型;

  2. 如果把引用类型转换为父类类型,则称为向上转型。

引用类型变量来访问所引用对象的属性和方法 绑定规则

PS:引用变量实际引用的对象 : new的谁 ; 引用变量所声明的类型 : 声明变量的类型

  1. 实例方法与引用变量实际引用的对象的方法进行绑定,这种绑定属于动态绑定,因为是在运行时由 Java 虚拟机动态决定的

  2. 静态方法与引用变量所声明的类型的方法绑定,这种绑定属于静态绑定,因为是在编译阶段已经做了绑定。

  3. 成员变量(包括静态变量和实例变量)与引用变量所声明的类型的成员变量绑定,这种绑定属于静态绑定,因为在编译阶段已经做了绑定

值得一提的是 private, final, static 都是与引用变量所声明的类型的方法,静态绑定

四、看几道题

  1. 变量内存的类型只能向下兼容,保证能够赋值其子类

    Person [] a = new Son[2];
    a[1] = new Son();
    Person b = new Son();
    b = new Person();
    a[1] = new Person();//运行错误
    

    为什么,看第一行,在堆中申请了一个长度为2的数组,每个数组的类型是Son,这也就说明数组的元素的引用类型指向的真实的实例类型只能是Son类(当然也包含Son的子类)
    而最后一行,给a[1]一个Son的引用变量赋值一个Person对象的内存地址,当然会运行错误。

  2. 向下转型的情况到底什么时候发生?

    Person a = new Son();
    Son son = (Son)a;
    son.fuck();//男孩能做fuck 动作
    son == a;//true
    

    son.fuck()这似乎也挺好理解的,是以Son角度调用它的,它也确实是Son的对象实例;但是son == a就很有意思了,Son强制转换并没有重新生成一个新的对象,而是告诉jvm,右边的(SON)a整体是Son型的,可以被赋值。

  3. 创建对象的过程

    public class Wangxiaosan extends Wangsan {
        private int age = 3;
        public Wangxiaosan(int age) {
            this.age = age;
            System.out.println("王小三的年龄:" + this.age);
        }
        
        public void write() { // 子类覆盖父类方法
            System.out.println("我小三上幼儿园的年龄是:" + this.age);
        }
        
        public static void main(String[] args) {
            new Wangxiaosan(4);
    //        上幼儿园之前
    //        我小三上幼儿园的年龄是:0
    //        上幼儿园之后
    //        王小三的年龄:4
        }
    }
    
    class Wangsan {
        Wangsan () {
            System.out.println("上幼儿园之前");
            write();
            System.out.println("上幼儿园之后");
        }
        public void write() {
            System.out.println("老子上幼儿园的年龄是3岁半");
        }
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值