05 类和对象的创建
在Java的世界里,必须先有类,才能有对象,这视乎和设计模式有关,如果你有玩过JavaScript,你就会发现其中的明显的区别
一、内存模式
参考文章:
- 内存中的堆和栈到底是什么 从汇编层面分析,栈和堆内存的情况,但是还是没有说为什么需要栈和堆
- JVM调优总结 其中一部分解释了关于为什么需要堆栈
现代高级语言基本都是分为含有下列部分:
- 静态存储区域:是由编译器自动分配和释放的,即内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,直到整个程序运行结束时才被释放,如全局变量与 static 变量
- 栈:在函数内顺序执行的代码中,声明变量,变量所代表的内存,是由堆空间分配的。一般表现为声明语句的左侧。(也经常称其为函数栈)
- 堆:称为动态内存,数据结构复杂
那Java里面是什么样的呢?
- 方法区(class,static变量)
- 栈 (变量)
- 堆 (对象)
字符串常量呢?
Java 中方法区与常量池
- 在 JDK6.0 及之前版本,字符串常量池是放在 Perm Gen 区(也就是方法区)中,此时常量池中存储的是对象。
- 在 JDK7.0 版本,字符串常量池被移到了堆中了。此时常量池存储的就是引用了。在 JDK8.0 中,永久代(方法区)被元空间取代了。
在新版本中,方法区被元空间取代?
永久代被替换原因、元空间特点、元空间内存查看分析方法
二、类和对象的初步了解
(1) 什么是类
具有相同特性(数据元素)和行为(功能)的对象的抽象就是类(当然,这并不意味者,类要在对象之后才有,我们可以虚构类:比如古代神话的神,就是一种虚构的类)
(2) 什么是对象
对象是人们要进行研究的任何具体事物
(3) 鸡和蛋的问题
先有对象还是先有类?OOP的思想是通过将现实生活的事物抽象成一个个类,从而符合人类思维的编程逻辑,但是如果现实世界没有的事物呢?
我们想象需要一个怪兽类,它没有事物参照,通过假象描述它的特点,创建出的怪兽类。在通过new创建具体的怪兽。
事实上,OOP完全可以先有类,再进行实例化,并且Java也是这么实现的,没有凭空出现的对象,都是通过实例化Object类创建的。实例化也会检查是否加载该类。
(4) 类和对象的关系
- 类是对象的模板,这就像生物中的DNA序列一样,要是这类物种你就应该包含这块DNA序列
- 该类的对象根据类模板创建生成,是类的实例
- 类是一种抽象,将一类事物的相同点抽离,收敛为类
- 可以对类继续抽象,即对类进行分类或者说扩展,关键词为extends,这种类叫做子类,也可叫做派生类;相对的被抽象的类叫做父类。
- Object是所有类的祖先类,任何一个类时候如果没有明确的继承一个父类的话,那么它就是Object的子类。
从这一点上,假设直接定义一个类叫Person,抽象出子类Son;那么Object是Person的父类,Object是Son的超类,Son是Person和Object的派生类。
从某种意义上说,Person是对Object的抽象,这样恰好的将整个逻辑连贯起来 - 实例 instanceof 类,判断实例是否属于该类
三、类的声明和对象的创建
初学者往往会把类和对象搞混淆,类是多个实例的抽象。在现实社会中,小时候你爱玩,你去商店玩具赛车。
老板问: 你要什么车车呀?(其实是问你要什么类型的赛车)
你说: 要遥控赛车。(遥控赛车是赛车的子类,对其的更加具体的抽象)
老板说: 遥控赛车的车是啥呀?怎么样的,包装(你就要描述遥控赛车这种车类的具体属性,还有操作,比如怎么操作,什么外型。做完这些你就声明了一个遥控赛车类)
你说: 老板来一款吧!(你发动了声明变量和new操作,要拿钱(你的栈内存和堆内存),买一辆遥控赛车(创建一个变量做遥控器、创建一个对象做车),通过遥控器让小车东起来(通过访问变量来访问这个对象))
(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 + "移动"); } }
-
我们尝试操作赛车,但是我们需要一个入口方法
public class Start() { public static void main() { const raeingCarTelecontrol = new RaeingCar(); raeingCarTelecontrol.direction = "上"; raeingCarTelecontrol.move(); } }
(2) 构造函数 => 对象的构造和初始化
创建类的实例,必须调用构造函数,但其主要作用是初始化对象
要注意的点:
-
函数名和对象同名
-
无修饰符或者是public
-
无返回值类型且不能是void(普通的方法必须要返回类型,即使没有也需要返回void)
class TEST { private String name; Test(String name) { this.name = name; } }
-
如果一个类没有显示声明构造函数,会执行默认的构造函数 className() {} 空的方法体
构造函数无论是外部还是类内部都无法被访问,它是类的一个特殊的成员
(3) 上面不是谁都知道吗,有没有更加具体,底层的过程呢?
下面细说一下,Java对象的创建过程:
大部分上面的博文已经提到的我就不说了,对一些进行补充:
- 创建对象,必须有类的声明的前提,自然会先检查类是否加载
- 创建对象,为其分配空间(分配前自然是扫描类的各个字段,方法永远是保存在类中即方法区,需要的时候绑定调用(多态)),初始化内存,各个字段被赋值初始值
- 创建对象先扫描父类,再扫描子类,new 子类的时候只会创建一个对象(如果成员变量有重名,该成员会添加父类名以示区分,不如Fanter.age和age都是对象的属性,并不会直接删除过滤
- 完成上面操作进行init,包括:变量初始化声明、语句块、构造函数执行。同样是由父到子。
三、is-A 和 has-A了解一下
- is-A代表了 实例 instanceof 类 的关系
- has-A代表了 实例.成员 的关系,成员包括:变量和方法
了解这个有什么作用?
判断是否正确编译和执行的依据
(1) 为什么向上转型能够成立? => 满足is-A的关系
你想象一下,就拿上面的遥控赛车讲解,假设我再在遥控赛车基础上
抽象出一个智能遥控赛车,比遥控赛车多了一个自动识别障碍物的功能,那你拿遥控赛车的遥控器能操作智能遥控赛车吗?
当然可以,应为智能遥控赛车已经包括了遥控赛车的功能(向上兼容),并且对遥控赛车的功能没有任何影响。
从上面其实体现了下面的思想:
- 子类是对父类功能的继承
- 子类的功能不会对父类功能产生影响(你可能说重写,不就改变了父类方法?方法的执行是动态绑定,我前面说过,如果出现相同的,会加以父类名称以便区分 => 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();//编译错误
}
}
- 对象引用变量son,它是将 new Son() 看成一个 Person 对象,这当然是合情合理的嘛,比如
son.eat(),引用变量son要求对象做eat操作,它当然可以通过编译,对于该对象来说:我不禁继承了Person的eat的行为,我还更能吃了(重写eat),它当然会执行自己的eat了。 - son.fuck(),该对象对于引用变量来说,它是一个 Person 对象的啊,怎么能有 fuck 方法,这明显不符合道理,编译错误
(3) 总结
动态绑定
在调用该方法时会根据调用时传入的参数进行解析,匹配,选择相应的函数调用(函数在方法区)
对象数据类型转换
PS: Java 语言允许某个类型的引用变量引用子类的实例,而且可以对这个引用变量进行类型转换。
-
如果把引用类型转换为子类类型,则称为向下转型;
-
如果把引用类型转换为父类类型,则称为向上转型。
引用类型变量来访问所引用对象的属性和方法 绑定规则
PS:引用变量实际引用的对象 : new的谁 ; 引用变量所声明的类型 : 声明变量的类型
-
实例方法与引用变量实际引用的对象的方法进行绑定,这种绑定属于动态绑定,因为是在运行时由 Java 虚拟机动态决定的
-
静态方法与引用变量所声明的类型的方法绑定,这种绑定属于静态绑定,因为是在编译阶段已经做了绑定。
-
成员变量(包括静态变量和实例变量)与引用变量所声明的类型的成员变量绑定,这种绑定属于静态绑定,因为在编译阶段已经做了绑定
值得一提的是 private, final, static 都是与引用变量所声明的类型的方法,静态绑定
四、看几道题
-
变量内存的类型只能向下兼容,保证能够赋值其子类
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对象的内存地址,当然会运行错误。 -
向下转型的情况到底什么时候发生?
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型的,可以被赋值。
-
创建对象的过程
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岁半"); } }