1、先有类?还是先有对象? 在开发过程中,其实是分为两个阶段的,一个是设计阶段,一个是实现阶段。 在设计阶段是我们通过场景,带入用例去分析,在这个时候这些用例都是以对象的方式出现的。然后在通过归纳发现具有相同属性和行为的对象,这个归纳是人脑的一种抽取,我们得到类的概念。 此时,是通过人脑的抽象把具有相同属性和行为的对象归纳为一类。
在实现过程中,代码是先定义类,然后通过类产生对象。这里体现的是编码中的复用思想,把类看成是对象“模版”,先定义好模版,然后通过模版进行复制,产生无数个对象。在具体使用的时候,一定是用对象而不是用类。 类是对象的模版,对象是类的具体的实际的存在。
2、类是一种数据类型 数据类型本身就是一个非常重要的概念,没有它,大部分编程语言是没有办法设计出来。这是因为软件的任务就是操作数据,而硬件提供的工作环境只有0和1组成的二进制。那么在现实场景当中需要用到的各种数据,最终都会化为0和1,那么在运算的时候如何分辨?如何进行不同的运算呢?就靠“数据类型”。
现实中的数据类型实在是太丰富了,且变化太多了。因此没有一门语言可以把所有的数据类型全部预设,只能预设最基本的数据类型。基本上都是有限的几个:数字型(有些会分整型和实型),字符型, 布尔型(有些没有)等等。然后再提供语法,让开放人员可以自定义更复杂的数据类型。
Java里面提出的就是“类”这个概念,它能保存更多的数据(属性),还能定义各种各样的操作方法。所以,我们完全可以把类描述为“复杂数据类型”。如果我们更看重它产生的变量和对象在内存中的保存形式,那么我们也可以把它称为“引用数据类型”。
3、类与类之间的关系 is - a 把对方做为自己的父类 has - a 把对方作为自己的属性 use - a 把对方作为自己某个方法的局部变量
在设计的时候,不要为了达到最终效果去做设计,而是要更多的去贴近现实场景的关联关系。虽然可能不符合场景关联的设计你也能绕出最终效果,但是如果场景发生变化,那么很可能你会推翻之前的设计全部重来。而符合场景的关联关系设计,可以随着场景的变化,其自身也可以符合这种变化,那么我们修改的量更少,可扩展性更强。
封装
这是两个动作,先装,后封。
装 在具体的代码实现上,就是类的定义! class 类名{ 在这对{}就是装的边界,里面就是可以在类的身上装的内容。 }
这些内容中,核心的是:属性、构造、行为。 属性 -- 值数据 构造 -- 是类的基本功能的表现 -- 类就是用来产生对象的。 行为 -- 类提供的功能
至于说再其他的: 初始化块 内部类
封,是信息的隐藏;这里的信息包括:数据隐藏,数据的格式隐藏,数据操作权限隐藏,业务行为的具体实现隐藏......
手段:访问修饰符,方法的定义,get/set等等等等,甚至几种手段的综合。
继承
继承是一种类与类的关系(is - a); 设计继承的目的同样是为了复用。
语法很简单: 1、extends 2、单继承 3、默认继承Object
内存实现: “内存叠加” -- 这是Java继承的内存核心实现。
在产生一个子类对象的时候,JVM会首先调用父类的构造方法,在内存中产生一个父类对象部分(里面包含了父类的信息内容),然后再调用子类自己的构造方法,在父类对象部分的下面叠加上子类特有部分,从而构成一个完整的子类对象。
细节上要注意: 1、这里并不意味着产生了两个对象(父类对象和子类对象),其实只产生了一个子类对象。只是这个子类对象里面有一个父类对象部分。 2、父类并不是所有的信息内容都会被子类继承,比如:构造方法不会继承;父类中的私有属性且该属性没有允许子类操作到的方法(get/set)。 3、子类可以再定义一个与父类完全一个样的属性(类型一样名称一样),父类的定义的是放在父类对象部分,子类定义的是放在子类特有部分,并不会出现覆盖的情况。使用的时候用super.属性和this.属性进行区分,当然出现这种设计是不合理的,没有必要把同一个属性定义两次。
单继承的选择是Java语言的设计者的选择,不是所有的面相对象语言都是单继承。选择了它就要承受它的好(继承树的层次结构清晰,构建大型结构的时候不会出现网状结构),也要接受它的不好(丰富度不够)。这也是Java要设计接口这种类型的原因,接口可以通过多实现弥补这个问题。
Object是所有类包括数组的父类,这个特性需要各位同学一定要重视,因为这说明Object当中的方法是公用性最强的方法,换句话说只要是一个Java语言当中的对象就应该拥有的方法。其中最重要的是:equals和toString。
equals方法从名字上看就是“判断是否相等”,但问题是什么算相等?在Java程序中有两种情况都可以说是否“相等”。一个是他们是不是一个对象;还有一个是他们两个对象的内容是否相等。 前者,其实运算符"=="号就是专用来判断该情况的。那么就没有必要再提供一个equals方法来做同样的事情,所以equals方法设计的本意就是用到第2种情况的(即判断两个对象的内容是否相等)。只是由于在根类Object中,并不知道每个子类的具体内容是什么,所以先人在默认实现的时候还是用了“==”号,它只能由子类的实现者去重写。
toString方法不是把对象转换成字符串,而是返回对象的字符串描述。同样是由于它是定义在Object当中的,Object不知道子类的信息当然也不知道该用什么样的字符串内容去描述子类对象。所以,先人只能采用默认的方式---“类型@16进制Hash值”。如果我们在子类当中希望用自己的方式去描述这个对象,那么就对toString方法进行重写。
多态
多态 --- 相同的行为,不同的实现。
方法的重载和方法的重写都是多态。因为在他们的要求中,都有方法名相同,而方法名相同代表的就是相同的行为。
重载 --- 是在一个类当中,拥有多个同名方法,参数不同,各有各的实现。 重写 --- 一定是在继承关系当中,有多个类。父类拥有的方法,子类也有同样的方法,但是各有各的实现。
无论是重载还是重写,都是静态多态。这里的静态和static一点关系都没有。这里的静指的是程序运行前,也就是说本次调用到底调的哪个对象的哪个方法是在编译期就确定了的。
动态多态将是以后各位同学大量使用的,原因有二:1、它才是真正的设计层面要求的多态效果;2、满足开闭原则。 这里的动态指的就是编译期不确定运行的效果,需要在运行期根据绑定的对象的不同,从而决定有什么样的效果。具体实现的手段:动态绑定技术 + 重写技术。
动态绑定技术 数据类型转换 基本数据类型讲过转换 1、不是所有基本数据类型之间都可以发生转换,boolean就不能和任何数据类型发生转换;
2、小类型的转大类型的 --- 自动类型转换 小类型是指空间小,范围小
3、大类型的转小类型 --- 强制类型转换 强制以后编译通过,但是运行的时候有风险,精度有可能会丢失。
引用数据类型进行转换 1、不是所有引用数据类型之间都可以发生转换,必须要有继承关系;
2、小类型转大类型的 --- 自动类型转换 小类型是子类类型,说白了就是范围小 -- 只是说有时候我们不叫它自动类型转换,我们叫它向上转换;
3、大类型的转小类型 --- 强制类型转换 强制以后编译通过,但运行时有风险,风险是抱异常ClassCastException。 要想让大类型转小类型成功,只能是大类型真的指向的该小类型的对象。
所以最后总结: 要想编译通过同时运行也通过,只有两种情况: 本类变量 = 本类对象; 父类变量 = 子类对象;
然后在用变量操作对象的属性和行为的时候,这两种情况是有差异的: 本类变量 = 本类对象; "变量."是可以看到这个对象身上所有访问修饰符允许的属性和行为。不管是定义在它的父类还是子类当中。
父类变量 = 子类对象; "变量."是只能看到这个对象身上访问修饰符允许的来自于父类的属性和行为。但是这不代表该对象身上没有来自于子类的属性和行为,有,但是你看不到而已。
那么,重写方法呢?记住一点,对象是什么,就执行谁的方法。对象是A子类,执行的就是A子类的实现;对象是B子类,执行的就是B子类的实现。
抽象
抽象类
当我们在做设计的时候,不断的进行父类的抽取。那么当达到某一个层次的时候,我们会发现我们只能确定这一层次的父类拥有某个行为,但不能确定它该如何实现了。
那么在书写这样的父类方法时,我们就只能把这个方法写成抽象方法了,然后交给子类去强制实现。
而一个类如果有一个抽象方法,那么这个类就不能产生对象了。因为对象是具体的实际的存在,不应该具备未确定的内容,所以这个类就只能是抽象类了。
细节知识点: 1、有抽象方法的类一定是抽象类; 抽象类不一定有抽象方法。 2、抽象类是有构造方法的; 抽象类除了不能产生对象以外,该有的都有;
有构造器
3、子类必须实现抽象类中的所有抽象方法,否则它也是抽象类。
接口
由于Java提供的类的继承是单继承,而单继承丧失掉的是丰富度,所以它不得不再单独设计一种新的数据类型和新的实现方式来进行弥补。
这个就引出了interface和implements。
接口的任务就是让没有继承关系的类也能共享行为,从而达到程序代码最大丰富度。
所以接口才在语法上有它的特征: 1、关键字变啦,interface和implements; 2、弱化接口的属性,更关注接口的行为; 属性只能是公共静态常量属性; 行为在默认情况下都是公共抽象方法,就是等着实现类去实现的。 至于说后来的接口里面可以书写实现了的静态方法或默认方法,都是后期的补充。 3、接口没有构造方法 因为它的设计目的就是绑定到类上面去使用,所以不用产生对象;
4、接口支持多态,可以用接口的引用指向实现类的对象;
接口 变量 = 实现类对象;
5、接口可以继承接口,这是为了能够组合各种接口的能力,从而减少我们的代码量。