几年前我姐三十岁生日,我准备了个礼物,功能就是脚本式的对话框交流加上打印机效果的祝福语显示。当时基本一点基础都没有,但我异常自信地在网上下了本安卓第一行代码和Java入门123 ,然后看着教程装好环境和 Android Studio 就开工了。最后花了大约两个星期把成品做了出来——其实能更早,是我的审美洁癖拖了一半的时间。程序源码我上传到仓库里了,链接:a-gift-for-my-sister
当时做完后一不做二不休,趁着课余时间也把那本 Java 入门书籍读完了。最近趁过年简单粗略地回忆一下那时候学过的知识,万一以后能用上呢。
面向过程与面向对象
- 面向过程的语言一般会将解决问题的过程拆成一个个方法,通过一个个方法的执行来解决问题。对于更复杂的工程,往往需要通过一个主要的入口方法,自顶向下的设计程序。因此牵一发而动全身,可维护性不够理想。以洗衣机为例,假如要清洗洗衣机里的一堆脏衣服。对面向过程的语言,执行流程大概是这样:
- 执行加洗衣粉方法。
- 执行加水方法。
- 执行洗衣服方法。
- 执行清洗方法。
- 执行烘干方法。
- 解决同一个问题,面向对象的语言往往会先抽象出对象,然后用对象执行方法的方式解决问题,且下次洗衣服我们还可以直接调用已有的洗衣服方法。对面向对象的语言,执行流程大概是这样:
- 先弄出两个对象:“洗衣机”对象和“人”对象。
- 针对对象“洗衣机”加入一些属性和方法:“洗衣服方法”、“清洗方法”、“烘干方法”。
- 针对对象“人”加入属性和方法:“加洗衣粉方法”、“加水方法”。
- 执行:人.加洗衣粉 $\longrightarrow$ 人.加水 $\longrightarrow$ 洗衣机.洗衣服 $\longrightarrow$ 洗衣机.清洗 $\longrightarrow$ 洗衣机.烘干。
类与对象
- 用public声明的类可以被其他类使用
- 类是我们对某事物进行抽象,并把抽象出来的东西封装为一种自定义数据类型,方便以后直接调用。
- 创建一个类的实体就是创建一个类的对象,实际上就是对一个类的具体化。
- 类文件中只定义属性名称,可以赋值也可以不赋值。但创建对象后可对类中赋予的初始值进行修改。
- 引用是一个对象的名字,就像指针一样指向一个对象。一个对象可以由无数个名字,每个名字多可以用来调用该对象。
public class BasicCar {
int speed = 60;
String name = "五菱宏光";
String direction = “长安大学方向";
}
public class Test1 {
public static void main(String[] args){
BasicCar car1 = new car(); //声明一个Car类的引用,并创建一个Car类实体
car1.speed = 60; //通过ACommonCar给Car对象各属性赋值
car1.name = "五菱宏光";
car1.direction = “长安大学方向";
}
}
方法
- 方法是类的功能,必须在类内定义。
- 方法可以操作类内的属性。
- 方法定义格式为
访问控制符+返回值+方法名+(参数){方法内容;}
。- 访问控制符控制权限。
- 返回值定义方法返回的数据类型,见例程方法
isOverspeed()
。 - 方法名……就是方法名。
- 参数见第 4 条以及例程方法
raiseSpeed()
。
- 参数注意事项:
- 多个参数之间使用逗号隔开。
- 形参是类中定义的方法参数。如例程中的
int speedup
。 - 实参是调用时传入的方法参数,如例程中的
5
。 - 实参可以是直接的数值,也可是一个变量,只要其值可赋给形参即可。
main
方法是 java 运行时的程序入口。static
关键字意为静态。- 修饰变量时,称该变量为类变量或静态变量。创建对象时不会创建静态变量,且可以直接通过类名静态变量。
- 修饰方法时,称该变量为类方法或静态方法。与类变量同,可以直接通过类名调用静态方法。
public class BasicCar {
int speed = 60;
String name = "五菱宏光";
String direction = “长安大学方向";
public void driveCar(){ //访问控制符+返回值+方法名+(参数)
speed = 30;
direction = "家";
}
public void raiseSpeed(int speedup){ //访问控制符+返回值+方法名+(参数)
int currentspeed = speed + speedup; //计算当前速度
speed = speed + speedup; //赋给speed属性
}
public boolean isOverspeed(){ //访问控制符+返回值+方法名+(参数)
if(speed > 80){
return true;
}
else{
return false; }
}
}
public class Test2 {
public static void main(String[] args){
BasicCar car2 = new Car();
car2.driveCar(); //通过car2调用driveCar()方法
System.out.print (car2.speed); //输出速度
car2.raiseSpeed(5); //通过car2调用raiseSpeed()方法
System.out.print (car2.speed); //输出新速度
isOverspeed = myCar.isOverspeed(); //通过car2调用isOverspeed()方法
if(isOverspeed){System.out.println ("汽车超速行驶中")}
}
}
继承
- 引入继承的原因:
- 假如之前的
BasicCar
开腻了想换辆车,新车除了包含老车的所有属性,还能摆脱行星重力井飞向太空。我们应该怎么做? - 方法一:在原
BasicCar
类中增加一个判断语句判断是否是新车,再增加新车的新属性。但万一以后再换能登陆火星的新车、能飞出太阳系的新车就会很麻烦。 - 方法二:一个类一个类地编写。但以后要对整个车族某项属性进行统一修改的话会很麻烦。且每个类中有大量重复代码,代码冗余量多,不够简洁。
- 假如之前的
- 因此引入继承,语法是
public class SpaceCar extends Car
,则子类可继承父类所有内容,然后再单独添加新的内容。
public class SpaceCar extends BasicCar {
public int BoosterAmount; //助推器数量
public void launchToSpace(int BoosterAmount){
int OrbitalAltitude = 100*BoosterAmount; //根据助推器数量计算发射后轨道高度
}
}
- 继承的注意事项:
- 继承不是简单的“直接享用”父类中所有的方法和属性。子类中能使用父类中哪些属性和方法主要由访问控制符控制。访问控制符对继承有很大的意义,语法规则比较复杂,暂时略过。
- Java 中所有的类只有一个父类。
- 所有的类,都直接或间接的继承自 Object 类。
- 创建子类对象时会自动创建其父类对象,并内嵌在子类对象中。
- 可以用父类引用指向子类对象,将子类对象伪装成父类对象。但需注意伪装之后不可调用子类独有内容,因为是引用类型决定了可被调用的内容。
覆盖
- 引入覆盖的原因:
- 假设我需要对
SpaceCar
继承的speedUp
方法进行一些升级。 - 方法一:直接修改
speedUp
方法,但显然不行,speedUp
方法是全部车辆通用的。 - 方法二:单独给跑车设置一个新的加速方法
newSpeedUp
,但这样不合适,因为车应该只有一种加速逻辑。 - 方法三:把
BasicCar
类里的speedUp
方法删了,每个新车类均定义自己的speedUp
方法。也行,但感觉有点不完美。
- 假设我需要对
- 因此引入覆盖,语法没有新内容,直接在
SpaceCar
类里再定义一个空的speedUp
方法里,把升级内容写进方法即可。 SpaceCar
类继承自BasicCar
类,BasicCar
类中也有一个speedUp
方法。方法签名一模一样,为什么没有冲突?- 在同一个类中,方法的签名是辨别方法的唯一标识。故一个类中不允许出现两个方法签名相同的方法。
- 方法签名不能相同是针对同一个类中而言,而
SpaceCar
类是在子类中添加一个和父类中某方法同名方法,故不会冲突。也即“覆盖”。
- 覆盖发生的注意事项:
- 子类必须把这个方法从父类中继承过来,这个与方法的访问控制符有关。
- 子类中方法的访问控制符赋予方法的访问权限,必须与父类中对应方法的访问权限相同或更宽松。大多数情况下,都应该使用与父类方法相同的访问控制符。
- 子类中方法的返回值类型必须能赋给父类方法的返回值类型。举例来说,如果父类中的方法返回值是
int
,则子类中对应方法的返回值就不允许是double
。
- 当覆盖遇到伪装引用时,例如
BasicCar Test3 = new SpaceCar()
,则运行伪装成BasicCar
的Test3.speedUp
会调用新的speedUp
方法。有点反直觉,原因如下:- 程序编译时,发现
Test3
是一个BasicCar
类引用,而BasicCar
类中正好定义有speedUp
方法,符合继承中提到的伪装引用执行规则,故编译通过。 - 程序运行时,Java 平台运行到
Test3.speedUp
时发现Test3
引用指向了SpaceCar
类对象,故会先去SpaceCar
类中寻找speedUp
方法,找不到再去父类中继续寻找。
- 程序编译时,发现
接口
- 没有接口的局限性:
- 假设现在需要监测那两辆车状态怎么办?首先新写一个
Status
类组合到BasicCar
类上用于描述状态,然后再新写一个Recorder
类,分别传入两辆车的引用即可。 - 那过几天多了一百来辆车怎么办?貌似只传入所有车的父类引用也行。
- 那系统中还有 100 个
Bike
类,200 个Skate
类等和BasicCar
类没继承关系的类呢?那就得每多一种类型改一次Recorder
类代码。
- 假设现在需要监测那两辆车状态怎么办?首先新写一个
- 接口的含义:
- 谈到接口,大多数人的印象可能是 PCI 接口之类的插槽。这是种错误的认知,当我们说某型号 PCI 接口时,指的是该型号插槽遵守了 PCI 规范,而具体的 PCI 插槽只是该型号 PCI 接口的实例,下图显示了这种抽象过程。
- 在 Java中,接口其实也是一种类型,它与类不同在于,类是一种具体的类型,包含了每个方法的代码;而接口是一种抽象的类型,它只包含方法的定义,没有包含方法的具体代码。因此与现实世界类似,必须要有一个类去实现这个接口。
- 接口的特点:
- 一个接口可以有无数种实现。
- 一个类实现一个接口时,类中必须提供接口中所有抽象方法的具体实现。
- 一个类实现一个接口时,其子类也会随之自动实现某接口。
- 一个类实现一个接口时,就拥有了接口的类型。
- 接口也可以有引用,并可以指向实现接口的类。
- 因此对前述问题,我们可以通过接口使
Recorder()
与xxxCar
完全分离,从而解决问题。- 定义一个
recordAble
接口,令任何一个实现了recordAble
接口的xxxCar
都能返回recordAble
接口要求的标准数据类型。 - 由于所有汽车类都实现了
recordAble
接口,故recordAble
引用可指向所有汽车类,并可通过recordAble
引用调用getStatus()
方法(参考覆盖与伪装部分)。 - 将
Recorder()
类中的传入形参改为recordAble
。 - 这样一来,车辆类型对
Recorder
完全透明,完全无需随程序扩展而修改。
- 定义一个
//定义接口语法
public abstract interface RecordeAble {
public abstract Status getStatus();
}
//实现接口语法
public class Bike implements RecordeAble {
public Status getStatus(){ \\实现接口规定方法
//具体实现
}
public xxx xxx(){ \\其他无关方法
//具体实现
}
}